Skip to content

Commit ad4cca0

Browse files
committed
✨ feat: 支持通过 webview 读取剪贴板中的截图;支持外部脚本调用 chatGPT 的时候同时打开 webview;优化交互
1 parent 0dea6b2 commit ad4cca0

File tree

30 files changed

+517
-142
lines changed

30 files changed

+517
-142
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "lowcode",
44
"description": "lowcode tool, support ChatGPT",
55
"author": "wjkang <[email protected]>",
6-
"version": "1.7.1",
6+
"version": "1.7.2",
77
"icon": "asset/icon.png",
88
"publisher": "wjkang",
99
"repository": "https://github.com/lowcoding/lowcode-vscode",
@@ -12,8 +12,8 @@
1212
"vscode:prepublish": "yarn run compile",
1313
"compile": "webpack --mode production",
1414
"compile:tsc": "tsc -p ./",
15-
"webview:dev": "yarn --cwd \"webview-react\" dev",
16-
"webview:build": "yarn --cwd \"webview-react\" build",
15+
"dev": "yarn --cwd \"webview-react\" dev",
16+
"build": "yarn --cwd \"webview-react\" build",
1717
"lint": "eslint src --ext ts",
1818
"watch": "webpack --mode development",
1919
"pretest": "yarn run compile:tsc",

src/commands/runSnippetScript.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getEnv, rootPath } from '../utils/vscodeEnv';
66
import { getInnerLibs } from '../utils/lib';
77
import { getOutputChannel } from '../utils/outputChannel';
88
import { createChatCompletionForScript } from '../utils/openai';
9+
import { getClipboardImage } from '../utils/clipboard';
910

1011
const { window } = vscode;
1112

@@ -42,6 +43,7 @@ export const registerRunSnippetScript = (context: vscode.ExtensionContext) => {
4243
log: getOutputChannel(),
4344
createChatCompletion: createChatCompletionForScript,
4445
materialPath: template!.path,
46+
getClipboardImage,
4547
code: '',
4648
};
4749
try {

src/context.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,6 @@ export const setRootPath = (rootPath: string) => {
2323
data.rootPath = rootPath;
2424
};
2525

26-
export const setExtensionPath = (extensionPath: string) => {
27-
data.extensionPath = extensionPath;
28-
};
29-
3026
export const setLastActiveTextEditorId = (activeTextEditorId: string) => {
3127
data.activeTextEditorId = activeTextEditorId;
3228
};

src/utils/clipboard.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { closeWebView, showWebView } from '../webview';
2+
import { emitter } from './emitter';
3+
4+
export const getClipboardImage = () =>
5+
new Promise<string>((resolve) => {
6+
showWebView({
7+
key: 'getClipboardImage',
8+
task: { task: 'getClipboardImage' },
9+
});
10+
emitter.on('clipboardImage', (data) => {
11+
emitter.off('clipboardImage');
12+
setTimeout(() => {
13+
closeWebView('getClipboardImage');
14+
}, 300);
15+
resolve(data);
16+
});
17+
});

src/utils/emitter.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
type EventType = string | symbol;
2+
3+
// An event handler can take an optional event argument
4+
// and should not return a value
5+
type Handler<T = unknown> = (event: T) => void;
6+
type WildcardHandler<T = Record<string, unknown>> = (
7+
type: keyof T,
8+
event: T[keyof T],
9+
) => void;
10+
11+
// An array of all currently registered event handlers for a type
12+
type EventHandlerList<T = unknown> = Array<Handler<T>>;
13+
type WildCardEventHandlerList<T = Record<string, unknown>> = Array<
14+
WildcardHandler<T>
15+
>;
16+
17+
// A map of event types and their corresponding event handlers.
18+
type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
19+
keyof Events | '*',
20+
EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>
21+
>;
22+
23+
interface Emitter<Events extends Record<EventType, unknown>> {
24+
all: EventHandlerMap<Events>;
25+
26+
on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
27+
on(type: '*', handler: WildcardHandler<Events>): void;
28+
29+
off<Key extends keyof Events>(
30+
type: Key,
31+
handler?: Handler<Events[Key]>,
32+
): void;
33+
off(type: '*', handler: WildcardHandler<Events>): void;
34+
35+
emit<Key extends keyof Events>(type: Key, event: Events[Key]): void;
36+
emit<Key extends keyof Events>(
37+
type: undefined extends Events[Key] ? Key : never,
38+
): void;
39+
}
40+
41+
/**
42+
* Mitt: Tiny (~200b) functional event emitter / pubsub.
43+
* @name mitt
44+
* @returns {Mitt}
45+
*/
46+
function mitt<Events extends Record<EventType, unknown>>(
47+
all?: EventHandlerMap<Events>,
48+
): Emitter<Events> {
49+
type GenericEventHandler =
50+
| Handler<Events[keyof Events]>
51+
| WildcardHandler<Events>;
52+
all = all || new Map();
53+
54+
return {
55+
/**
56+
* A Map of event names to registered handler functions.
57+
*/
58+
all,
59+
60+
/**
61+
* Register an event handler for the given type.
62+
* @param {string|symbol} type Type of event to listen for, or `'*'` for all events
63+
* @param {Function} handler Function to call in response to given event
64+
* @memberOf mitt
65+
*/
66+
on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
67+
const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
68+
if (handlers) {
69+
handlers.push(handler);
70+
} else {
71+
all!.set(type, [handler] as EventHandlerList<Events[keyof Events]>);
72+
}
73+
},
74+
75+
/**
76+
* Remove an event handler for the given type.
77+
* If `handler` is omitted, all handlers of the given type are removed.
78+
* @param {string|symbol} type Type of event to unregister `handler` from (`'*'` to remove a wildcard handler)
79+
* @param {Function} [handler] Handler function to remove
80+
* @memberOf mitt
81+
*/
82+
off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) {
83+
const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
84+
if (handlers) {
85+
if (handler) {
86+
// eslint-disable-next-line no-bitwise
87+
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
88+
} else {
89+
all!.set(type, []);
90+
}
91+
}
92+
},
93+
94+
/**
95+
* Invoke all handlers for the given type.
96+
* If present, `'*'` handlers are invoked after type-matched handlers.
97+
*
98+
* Note: Manually firing '*' handlers is not supported.
99+
*
100+
* @param {string|symbol} type The event type to invoke
101+
* @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler
102+
* @memberOf mitt
103+
*/
104+
emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
105+
let handlers = all!.get(type);
106+
if (handlers) {
107+
(handlers as EventHandlerList<Events[keyof Events]>)
108+
.slice()
109+
.map((handler) => {
110+
handler(evt!);
111+
});
112+
}
113+
114+
handlers = all!.get('*');
115+
if (handlers) {
116+
(handlers as WildCardEventHandlerList<Events>)
117+
.slice()
118+
.map((handler) => {
119+
handler(type, evt!);
120+
});
121+
}
122+
},
123+
};
124+
}
125+
126+
type Events = {
127+
clipboardImage: string;
128+
chatGPTChunck: { text?: string; hasMore: boolean };
129+
chatGPTComplete: string;
130+
};
131+
132+
export const emitter = mitt<Events>();

src/utils/openai.ts

Lines changed: 77 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as https from 'https';
22
import { TextDecoder } from 'util';
33
import { getChatGPTConfig } from './config';
4+
import { emitter } from './emitter';
5+
import { showChatGPTView } from '../webview';
46

57
export const createChatCompletion = (options: {
68
apiKey: string;
@@ -43,29 +45,46 @@ export const createChatCompletion = (options: {
4345
}
4446
if (element.includes('data: ')) {
4547
if (element.includes('[DONE]')) {
46-
options.handleChunk &&
48+
if (options.handleChunk) {
4749
options.handleChunk({ hasMore: true, text: '' });
50+
emitter.emit('chatGPTChunck', { hasMore: true, text: '' });
51+
}
4852
return;
4953
}
5054
// remove 'data: '
5155
const data = JSON.parse(element.replace('data: ', ''));
5256
if (data.finish_reason === 'stop') {
53-
options.handleChunk &&
57+
if (options.handleChunk) {
5458
options.handleChunk({ hasMore: true, text: '' });
59+
emitter.emit('chatGPTChunck', { hasMore: true, text: '' });
60+
}
5561
return;
5662
}
5763
const openaiRes = data.choices[0].delta.content;
5864
if (openaiRes) {
59-
options.handleChunk &&
65+
if (options.handleChunk) {
6066
options.handleChunk({
6167
text: openaiRes.replaceAll('\\n', '\n'),
6268
hasMore: true,
6369
});
70+
emitter.emit('chatGPTChunck', {
71+
text: openaiRes.replaceAll('\\n', '\n'),
72+
hasMore: true,
73+
});
74+
}
6475
combinedResult += openaiRes;
6576
}
6677
} else {
67-
options.handleChunk &&
68-
options.handleChunk({ hasMore: true, text: element });
78+
if (options.handleChunk) {
79+
options.handleChunk({
80+
hasMore: true,
81+
text: element,
82+
});
83+
emitter.emit('chatGPTChunck', {
84+
hasMore: true,
85+
text: element,
86+
});
87+
}
6988
return;
7089
}
7190
} catch (e) {
@@ -78,16 +97,33 @@ export const createChatCompletion = (options: {
7897
}
7998
});
8099
res.on('error', (e) => {
81-
options.handleChunk &&
82-
options.handleChunk({ hasMore: true, text: e.toString() });
100+
if (options.handleChunk) {
101+
options.handleChunk({
102+
hasMore: true,
103+
text: e.toString(),
104+
});
105+
emitter.emit('chatGPTChunck', {
106+
hasMore: true,
107+
text: e.toString(),
108+
});
109+
}
83110
reject(e);
84111
});
85112
res.on('end', () => {
86113
if (error !== '发生错误:') {
87-
options.handleChunk &&
88-
options.handleChunk({ hasMore: true, text: error });
114+
if (options.handleChunk) {
115+
options.handleChunk({
116+
hasMore: true,
117+
text: error,
118+
});
119+
emitter.emit('chatGPTChunck', {
120+
hasMore: true,
121+
text: error,
122+
});
123+
}
89124
}
90125
resolve(combinedResult || error);
126+
emitter.emit('chatGPTComplete', combinedResult || error);
91127
});
92128
},
93129
);
@@ -101,6 +137,7 @@ export const createChatCompletion = (options: {
101137
options.handleChunk &&
102138
options.handleChunk({ hasMore: true, text: error.toString() });
103139
resolve(error.toString());
140+
emitter.emit('chatGPTComplete', error.toString());
104141
});
105142
request.write(JSON.stringify(body));
106143
request.end();
@@ -109,15 +146,37 @@ export const createChatCompletion = (options: {
109146
export const createChatCompletionForScript = (options: {
110147
messages: { role: 'system' | 'user' | 'assistant'; content: string }[];
111148
handleChunk?: (data: { text?: string; hasMore: boolean }) => void;
149+
showWebview?: boolean;
112150
}) => {
113-
const config = getChatGPTConfig();
114-
return createChatCompletion({
115-
hostname: config.hostname,
116-
apiPath: config.apiPath,
117-
apiKey: config.apiKey,
118-
model: config.model,
119-
messages: options.messages,
120-
maxTokens: config.maxTokens,
121-
handleChunk: options.handleChunk,
151+
if (!options.showWebview) {
152+
const config = getChatGPTConfig();
153+
return createChatCompletion({
154+
hostname: config.hostname,
155+
apiPath: config.apiPath,
156+
apiKey: config.apiKey,
157+
model: config.model,
158+
messages: options.messages,
159+
maxTokens: config.maxTokens,
160+
handleChunk: options.handleChunk,
161+
});
162+
}
163+
// 打开 webview,使用 emitter 监听结果,把结果回传给 script
164+
showChatGPTView({
165+
task: {
166+
task: 'askChatGPT',
167+
data: options.messages.map((m) => m.content).join('\n'),
168+
},
169+
});
170+
return new Promise<string>((resolve, reject) => {
171+
emitter.on('chatGPTChunck', (data) => {
172+
if (options.handleChunk) {
173+
options.handleChunk(data);
174+
}
175+
});
176+
emitter.on('chatGPTComplete', (data) => {
177+
resolve(data);
178+
emitter.off('chatGPTChunck');
179+
emitter.off('chatGPTComplete');
180+
});
122181
});
123182
};

src/webview/controllers/script.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const runScript = async (
1414
createBlockPath?: string;
1515
script: string;
1616
params: string;
17+
clipboardImage?: string;
1718
model: object;
1819
}>,
1920
) => {
@@ -25,6 +26,7 @@ export const runScript = async (
2526
const context = {
2627
model: message.data.model,
2728
params: message.data.params,
29+
clipboardImage: message.data.clipboardImage,
2830
vscode,
2931
workspaceRootPath: rootPath,
3032
env: getEnv(),

src/webview/controllers/task.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as vscode from 'vscode';
22
import { IMessage } from '../type';
3+
import { emitter } from '../../utils/emitter';
34

45
export const getTask = async (
56
message: IMessage,
@@ -8,3 +9,14 @@ export const getTask = async (
89
task: { task: string; data?: any };
910
},
1011
) => context.task;
12+
13+
export const putClipboardImage = async (
14+
message: IMessage<string>,
15+
context: {
16+
webview: vscode.Webview;
17+
task: { task: string; data?: any };
18+
},
19+
) => {
20+
emitter.emit('clipboardImage', message.data);
21+
return true;
22+
};

0 commit comments

Comments
 (0)