agent-bridge-qwenwork 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/config.schema.json +14 -0
- package/dist/src-ts/agent.js +10 -0
- package/dist/src-ts/plugins/qwenwork/cdp-client.js +165 -0
- package/dist/src-ts/plugins/qwenwork/index.js +121 -0
- package/dist/src-ts/plugins/qwenwork/openai-adapter.js +146 -0
- package/package.json +21 -4
- package/plugin.json +10 -0
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 千问办公插件
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
供 `mu-agent-bridge` 使用的千问办公插件。核心包已内置此插件;独立包适用于单独发布和安装。
|
|
4
|
+
|
|
5
|
+
安装:`npm install -g agent-bridge-qwenwork`。插件需要 Windows 千问办公开启 CDP 端口 `9226`。
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"aid": { "type": "string", "description": "复用的 EvolCore 已有 AID" },
|
|
6
|
+
"cdpPort": { "type": "integer", "minimum": 1, "maximum": 65535, "default": 9226 },
|
|
7
|
+
"httpPort": { "type": "integer", "minimum": 0, "maximum": 65535, "default": 17893 },
|
|
8
|
+
"subChatId": { "type": "string" },
|
|
9
|
+
"chatId": { "type": "string" },
|
|
10
|
+
"workspace": { "type": "string" },
|
|
11
|
+
"timeoutMs": { "type": "integer", "minimum": 1000, "default": 240000 }
|
|
12
|
+
},
|
|
13
|
+
"additionalProperties": false
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AgentError = void 0;
|
|
4
|
+
class AgentError extends Error {
|
|
5
|
+
constructor(message, options) {
|
|
6
|
+
super(message, options);
|
|
7
|
+
this.name = "AgentError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
exports.AgentError = AgentError;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QwenCdpClient = exports.inputSelector = exports.assistantMessageSelector = void 0;
|
|
4
|
+
exports.freshAssistantMessages = freshAssistantMessages;
|
|
5
|
+
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
6
|
+
exports.assistantMessageSelector = '[data-message-id][data-part-type="text"]:not([data-conversation-user-text="true"])';
|
|
7
|
+
exports.inputSelector = '.chat-input-editor-text[contenteditable="true"]';
|
|
8
|
+
function freshAssistantMessages(beforeIds, messages) {
|
|
9
|
+
return messages.filter((message) => Boolean(message.id) && !beforeIds.has(message.id) && Boolean(message.text?.trim()));
|
|
10
|
+
}
|
|
11
|
+
function wait(ms, signal) {
|
|
12
|
+
if (signal?.aborted)
|
|
13
|
+
return Promise.reject(new Error("请求已取消"));
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const timer = setTimeout(resolve, ms);
|
|
16
|
+
signal?.addEventListener("abort", () => {
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
reject(new Error("请求已取消"));
|
|
19
|
+
}, { once: true });
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** CDP/tRPC transport for the QwenWorkCN desktop client. */
|
|
23
|
+
class QwenCdpClient {
|
|
24
|
+
socket;
|
|
25
|
+
sequence = 0;
|
|
26
|
+
pending = new Map();
|
|
27
|
+
options;
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
this.options = {
|
|
30
|
+
port: options.port ?? Number(process.env.QWEN_CDP_PORT ?? "9226"),
|
|
31
|
+
subChatId: options.subChatId ?? process.env.QWEN_SUBCHAT ?? "muj55ep9426gudjw",
|
|
32
|
+
chatId: options.chatId ?? process.env.QWEN_CHAT ?? "muj55ep9u6wbv0eo",
|
|
33
|
+
workspace: options.workspace ?? `${process.env.APPDATA ?? ""}\\QwenWorkCN\\workspace`,
|
|
34
|
+
timeoutMs: options.timeoutMs ?? 30_000,
|
|
35
|
+
fetchImpl: options.fetchImpl,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
connected() { return Boolean(this.socket); }
|
|
39
|
+
async connect() {
|
|
40
|
+
if (this.socket)
|
|
41
|
+
return;
|
|
42
|
+
const fetchImpl = this.options.fetchImpl ?? fetch;
|
|
43
|
+
const response = await fetchImpl(`http://127.0.0.1:${this.options.port}/json`);
|
|
44
|
+
if (!response.ok)
|
|
45
|
+
throw new Error(`CDP /json HTTP ${response.status}`);
|
|
46
|
+
const targets = await response.json();
|
|
47
|
+
const page = targets.find((target) => target.type === "page" && (target.url ?? "").includes("index.html#windowId=main"))
|
|
48
|
+
?? targets.find((target) => target.type === "page" && (target.url ?? "").includes("index.html"));
|
|
49
|
+
if (!page?.webSocketDebuggerUrl)
|
|
50
|
+
throw new Error(`未找到千问主窗口 target(客户端是否以 --remote-debugging-port=${this.options.port} 启动?)`);
|
|
51
|
+
this.socket = new WebSocket(page.webSocketDebuggerUrl);
|
|
52
|
+
this.socket.onmessage = (event) => this.onMessage(String(event.data));
|
|
53
|
+
await new Promise((resolve, reject) => {
|
|
54
|
+
const socket = this.socket;
|
|
55
|
+
socket.onopen = () => resolve();
|
|
56
|
+
socket.onerror = () => reject(new Error("CDP WebSocket 连接失败"));
|
|
57
|
+
});
|
|
58
|
+
await this.evaluate(`(() => {
|
|
59
|
+
if (!window.__ts_bridge_resps) {
|
|
60
|
+
window.__ts_bridge_resps = [];
|
|
61
|
+
window.electronTRPC.onMessage(message => window.__ts_bridge_resps.push(message));
|
|
62
|
+
}
|
|
63
|
+
return 'ok';
|
|
64
|
+
})()`);
|
|
65
|
+
}
|
|
66
|
+
close() {
|
|
67
|
+
this.socket?.close();
|
|
68
|
+
this.socket = undefined;
|
|
69
|
+
for (const pending of this.pending.values())
|
|
70
|
+
pending.reject(new Error("CDP 已关闭"));
|
|
71
|
+
this.pending.clear();
|
|
72
|
+
}
|
|
73
|
+
async send(text) {
|
|
74
|
+
return this.trpc("agent.submitMessage", "mutation", {
|
|
75
|
+
subChatId: this.options.subChatId,
|
|
76
|
+
chatId: this.options.chatId,
|
|
77
|
+
prompt: text,
|
|
78
|
+
cwd: this.options.workspace,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async direct(text, timeoutMs = 240_000, signal) {
|
|
82
|
+
const snapshot = await this.evaluate(`JSON.stringify([...document.querySelectorAll(${JSON.stringify(exports.assistantMessageSelector)})].map(element => ({ id: element.getAttribute('data-message-id'), text: element.innerText })))`);
|
|
83
|
+
const before = JSON.parse(snapshot || "[]");
|
|
84
|
+
const beforeIds = new Set(before.map((message) => message.id).filter((id) => Boolean(id)));
|
|
85
|
+
const focused = await this.evaluate(`(() => {
|
|
86
|
+
const element = document.querySelector(${JSON.stringify(exports.inputSelector)});
|
|
87
|
+
if (!element) return false;
|
|
88
|
+
element.focus();
|
|
89
|
+
return true;
|
|
90
|
+
})()`);
|
|
91
|
+
if (!focused)
|
|
92
|
+
throw new Error("千问输入框未找到");
|
|
93
|
+
await this.cdp("Input.insertText", { text });
|
|
94
|
+
for (const type of ["keyDown", "keyUp"]) {
|
|
95
|
+
await this.cdp("Input.dispatchKeyEvent", {
|
|
96
|
+
type, key: "Enter", code: "Enter", text: type === "keyDown" ? "\r" : "", unmodifiedText: type === "keyDown" ? "\r" : "",
|
|
97
|
+
windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
let previousReply = "";
|
|
101
|
+
let stablePolls = 0;
|
|
102
|
+
const deadline = Date.now() + timeoutMs;
|
|
103
|
+
while (Date.now() < deadline) {
|
|
104
|
+
await wait(2_000, signal);
|
|
105
|
+
const messagesJson = await this.evaluate(`JSON.stringify([...document.querySelectorAll(${JSON.stringify(exports.assistantMessageSelector)})].map(element => ({ id: element.getAttribute('data-message-id'), text: element.innerText })))`);
|
|
106
|
+
const fresh = freshAssistantMessages(beforeIds, JSON.parse(messagesJson || "[]"));
|
|
107
|
+
const reply = fresh.at(-1)?.text.trim() ?? "";
|
|
108
|
+
stablePolls = reply && reply === previousReply ? stablePolls + 1 : 0;
|
|
109
|
+
previousReply = reply;
|
|
110
|
+
if (reply && stablePolls >= 3)
|
|
111
|
+
return reply;
|
|
112
|
+
}
|
|
113
|
+
throw new Error("千问办公 UI 超时未返回回复");
|
|
114
|
+
}
|
|
115
|
+
onMessage(raw) {
|
|
116
|
+
let message;
|
|
117
|
+
try {
|
|
118
|
+
message = JSON.parse(raw);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (message.id === undefined)
|
|
124
|
+
return;
|
|
125
|
+
const pending = this.pending.get(message.id);
|
|
126
|
+
if (!pending)
|
|
127
|
+
return;
|
|
128
|
+
this.pending.delete(message.id);
|
|
129
|
+
pending.resolve(message);
|
|
130
|
+
}
|
|
131
|
+
async cdp(method, params = {}, timeoutMs = this.options.timeoutMs) {
|
|
132
|
+
if (!this.socket)
|
|
133
|
+
throw new Error("CDP 尚未连接");
|
|
134
|
+
const id = ++this.sequence;
|
|
135
|
+
const result = new Promise((resolve, reject) => this.pending.set(id, { resolve, reject }));
|
|
136
|
+
this.socket.send(JSON.stringify({ id, method, params }));
|
|
137
|
+
return Promise.race([result, sleep(timeoutMs).then(() => { throw new Error(`CDP ${method} 超时`); })]);
|
|
138
|
+
}
|
|
139
|
+
async evaluate(expression) {
|
|
140
|
+
const response = await this.cdp("Runtime.evaluate", { expression, returnByValue: true, awaitPromise: true });
|
|
141
|
+
if (response.result?.exceptionDetails)
|
|
142
|
+
throw new Error(`页面 JS 异常: ${JSON.stringify(response.result.exceptionDetails).slice(0, 300)}`);
|
|
143
|
+
return response.result?.result?.value;
|
|
144
|
+
}
|
|
145
|
+
async trpc(path, type, input) {
|
|
146
|
+
const id = `ts-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
147
|
+
const operation = { id, type, path, input: { json: input, meta: {} } };
|
|
148
|
+
await this.evaluate(`(() => { window.electronTRPC.sendMessage({ method: 'request', operation: ${JSON.stringify(operation)} }); return 'sent'; })()`);
|
|
149
|
+
const deadline = Date.now() + 20_000;
|
|
150
|
+
while (Date.now() < deadline) {
|
|
151
|
+
await wait(400);
|
|
152
|
+
const encoded = await this.evaluate(`(() => {
|
|
153
|
+
const list = window.__ts_bridge_resps ?? [];
|
|
154
|
+
const index = list.findIndex(message => message && message.id === ${JSON.stringify(id)});
|
|
155
|
+
if (index < 0) return null;
|
|
156
|
+
const [message] = list.splice(index, 1);
|
|
157
|
+
return JSON.stringify(message);
|
|
158
|
+
})()`);
|
|
159
|
+
if (encoded && encoded !== "null")
|
|
160
|
+
return JSON.parse(String(encoded));
|
|
161
|
+
}
|
|
162
|
+
throw new Error(`tRPC ${path} 超时`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
exports.QwenCdpClient = QwenCdpClient;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QwenworkPlugin = exports.qwenworkManifest = void 0;
|
|
4
|
+
exports.createQwenworkPlugin = createQwenworkPlugin;
|
|
5
|
+
const openai_adapter_1 = require("./openai-adapter");
|
|
6
|
+
const cdp_client_1 = require("./cdp-client");
|
|
7
|
+
exports.qwenworkManifest = {
|
|
8
|
+
id: "qwenwork",
|
|
9
|
+
version: "1.0.0",
|
|
10
|
+
displayName: "千问办公",
|
|
11
|
+
entry: "dist/index.js",
|
|
12
|
+
apiVersion: "1",
|
|
13
|
+
process: "in-process",
|
|
14
|
+
capabilities: ["chat", "openai-compatible", "health"],
|
|
15
|
+
configSchema: "config.schema.json",
|
|
16
|
+
defaults: { cdpPort: 9226, httpPort: 17893 },
|
|
17
|
+
};
|
|
18
|
+
function asNumber(value, fallback) {
|
|
19
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
20
|
+
}
|
|
21
|
+
/** Dynamic qwenwork plugin. It returns text to the platform and never emits AUN messages itself. */
|
|
22
|
+
class QwenworkPlugin {
|
|
23
|
+
manifest = exports.qwenworkManifest;
|
|
24
|
+
injectedClient;
|
|
25
|
+
client;
|
|
26
|
+
adapter;
|
|
27
|
+
configuredCdpPort;
|
|
28
|
+
activeCdpPort;
|
|
29
|
+
configuredHttpPort;
|
|
30
|
+
disposers = [];
|
|
31
|
+
requestQueue = Promise.resolve();
|
|
32
|
+
state = "stopped";
|
|
33
|
+
message = "";
|
|
34
|
+
constructor(options = {}) {
|
|
35
|
+
this.injectedClient = options.client;
|
|
36
|
+
this.configuredCdpPort = options.cdpPort;
|
|
37
|
+
this.configuredHttpPort = options.httpPort;
|
|
38
|
+
}
|
|
39
|
+
async activate(context) {
|
|
40
|
+
if (this.state === "ready")
|
|
41
|
+
return;
|
|
42
|
+
this.state = "starting";
|
|
43
|
+
try {
|
|
44
|
+
const config = context.config;
|
|
45
|
+
this.activeCdpPort = asNumber(config.cdpPort, this.configuredCdpPort ?? exports.qwenworkManifest.defaults.cdpPort);
|
|
46
|
+
const client = this.injectedClient ?? new cdp_client_1.QwenCdpClient({
|
|
47
|
+
port: this.activeCdpPort,
|
|
48
|
+
subChatId: typeof config.subChatId === "string" ? config.subChatId : undefined,
|
|
49
|
+
chatId: typeof config.chatId === "string" ? config.chatId : undefined,
|
|
50
|
+
workspace: typeof config.workspace === "string" ? config.workspace : undefined,
|
|
51
|
+
});
|
|
52
|
+
this.client = client;
|
|
53
|
+
await client.connect();
|
|
54
|
+
this.adapter = new openai_adapter_1.OpenAIAdapter((prompt, signal) => this.enqueue(() => client.direct(prompt, asNumber(config.timeoutMs, 240_000), signal)), {
|
|
55
|
+
onLog: (message) => context.logger?.info(message),
|
|
56
|
+
onExchange: (direction, text, peer, agentAid) => context.logger?.info("千问通信", { direction, text, peer, agentAid }),
|
|
57
|
+
});
|
|
58
|
+
if (context.http) {
|
|
59
|
+
this.registerRoutes(context.http);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
this.configuredHttpPort = asNumber(config.httpPort, this.configuredHttpPort ?? exports.qwenworkManifest.defaults.httpPort);
|
|
63
|
+
if (this.configuredHttpPort > 0)
|
|
64
|
+
await this.adapter.listen(this.configuredHttpPort);
|
|
65
|
+
}
|
|
66
|
+
this.state = "ready";
|
|
67
|
+
this.message = "";
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
this.state = "failed";
|
|
71
|
+
this.message = error instanceof Error ? error.message : String(error);
|
|
72
|
+
context.logger?.error(this.message);
|
|
73
|
+
await this.cleanup();
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async deactivate(_reason) {
|
|
78
|
+
await this.cleanup();
|
|
79
|
+
this.state = "stopped";
|
|
80
|
+
}
|
|
81
|
+
async health() {
|
|
82
|
+
const details = { cdpPort: this.activeCdpPort ?? exports.qwenworkManifest.defaults.cdpPort, httpPort: this.configuredHttpPort ?? exports.qwenworkManifest.defaults.httpPort };
|
|
83
|
+
if (this.state === "ready" && this.client && !this.client.connected())
|
|
84
|
+
return { status: "degraded", message: "千问 CDP 已断开", details };
|
|
85
|
+
return { status: this.state, ...(this.message ? { message: this.message } : {}), details };
|
|
86
|
+
}
|
|
87
|
+
async handle(request) {
|
|
88
|
+
if (this.state !== "ready" || !this.client)
|
|
89
|
+
throw new Error("qwenwork 插件未就绪");
|
|
90
|
+
const text = await this.enqueue(() => this.client.direct(request.task, request.timeoutMs ?? 240_000));
|
|
91
|
+
return { text };
|
|
92
|
+
}
|
|
93
|
+
registerRoutes(http) {
|
|
94
|
+
if (!this.adapter)
|
|
95
|
+
return;
|
|
96
|
+
const handler = (req, res) => this.adapter.handle(req, res);
|
|
97
|
+
for (const [method, path] of [["GET", "/v1/models"], ["POST", "/v1/chat/completions"], ["OPTIONS", "/v1/chat/completions"]]) {
|
|
98
|
+
const disposer = http.register({ method, path, handler });
|
|
99
|
+
if (typeof disposer === "function")
|
|
100
|
+
this.disposers.push(disposer);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async cleanup() {
|
|
104
|
+
for (const dispose of this.disposers.splice(0))
|
|
105
|
+
dispose();
|
|
106
|
+
await this.adapter?.close();
|
|
107
|
+
this.adapter = undefined;
|
|
108
|
+
this.client?.close();
|
|
109
|
+
this.client = undefined;
|
|
110
|
+
}
|
|
111
|
+
enqueue(operation) {
|
|
112
|
+
const run = this.requestQueue.then(operation, operation);
|
|
113
|
+
this.requestQueue = run.then(() => undefined, () => undefined);
|
|
114
|
+
return run;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
exports.QwenworkPlugin = QwenworkPlugin;
|
|
118
|
+
function createQwenworkPlugin(options = {}) {
|
|
119
|
+
return new QwenworkPlugin(options);
|
|
120
|
+
}
|
|
121
|
+
exports.default = createQwenworkPlugin;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OpenAIAdapter = void 0;
|
|
4
|
+
exports.messageText = messageText;
|
|
5
|
+
const node_http_1 = require("node:http");
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
function json(res, status, value) {
|
|
8
|
+
const body = JSON.stringify(value);
|
|
9
|
+
res.statusCode = status;
|
|
10
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
11
|
+
res.setHeader("Content-Length", Buffer.byteLength(body));
|
|
12
|
+
res.end(body);
|
|
13
|
+
}
|
|
14
|
+
function messageText(content) {
|
|
15
|
+
if (typeof content === "string")
|
|
16
|
+
return content;
|
|
17
|
+
if (Array.isArray(content)) {
|
|
18
|
+
return content.map((part) => {
|
|
19
|
+
if (typeof part === "string")
|
|
20
|
+
return part;
|
|
21
|
+
if (part && typeof part === "object" && "text" in part)
|
|
22
|
+
return String(part.text ?? "");
|
|
23
|
+
return "";
|
|
24
|
+
}).join("");
|
|
25
|
+
}
|
|
26
|
+
return content == null ? "" : String(content);
|
|
27
|
+
}
|
|
28
|
+
async function readBody(req) {
|
|
29
|
+
const chunks = [];
|
|
30
|
+
let size = 0;
|
|
31
|
+
for await (const chunk of req) {
|
|
32
|
+
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
33
|
+
size += data.length;
|
|
34
|
+
if (size > 2 * 1024 * 1024)
|
|
35
|
+
throw new Error("请求体超过 2 MiB");
|
|
36
|
+
chunks.push(data);
|
|
37
|
+
}
|
|
38
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
39
|
+
}
|
|
40
|
+
/** OpenAI-compatible HTTP adapter used by qwenwork and the platform router. */
|
|
41
|
+
class OpenAIAdapter {
|
|
42
|
+
complete;
|
|
43
|
+
model;
|
|
44
|
+
onLog;
|
|
45
|
+
onExchange;
|
|
46
|
+
prefix;
|
|
47
|
+
server;
|
|
48
|
+
constructor(complete, options = {}) {
|
|
49
|
+
this.complete = complete;
|
|
50
|
+
this.model = options.model ?? "qwenwork";
|
|
51
|
+
this.onLog = options.onLog;
|
|
52
|
+
this.onExchange = options.onExchange;
|
|
53
|
+
this.prefix = (options.prefix ?? "/v1").replace(/\/+$/, "") || "/v1";
|
|
54
|
+
}
|
|
55
|
+
models() {
|
|
56
|
+
return { object: "list", data: [{ id: this.model, object: "model", owned_by: "qwenwork" }] };
|
|
57
|
+
}
|
|
58
|
+
async handle(req, res) {
|
|
59
|
+
const requestPath = new URL(req.url ?? "/", "http://127.0.0.1").pathname;
|
|
60
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
61
|
+
if (req.method === "OPTIONS") {
|
|
62
|
+
res.setHeader("Access-Control-Allow-Headers", "authorization, content-type");
|
|
63
|
+
res.statusCode = 204;
|
|
64
|
+
res.end();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (req.method === "GET" && requestPath === `${this.prefix}/models`) {
|
|
68
|
+
json(res, 200, this.models());
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (req.method !== "POST" || requestPath !== `${this.prefix}/chat/completions`) {
|
|
72
|
+
json(res, 404, { error: { message: "Not found", type: "invalid_request_error" } });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const abort = new AbortController();
|
|
76
|
+
const peer = req.headers["x-aun-peer"];
|
|
77
|
+
const peerId = typeof peer === "string" && peer.length <= 256 ? peer : undefined;
|
|
78
|
+
const receivingAid = req.headers["x-evolcore-agent-aid"];
|
|
79
|
+
const agentAid = typeof receivingAid === "string" && receivingAid.length <= 256 ? receivingAid : undefined;
|
|
80
|
+
const onClose = () => { if (!res.writableEnded)
|
|
81
|
+
abort.abort(); };
|
|
82
|
+
res.once("close", onClose);
|
|
83
|
+
try {
|
|
84
|
+
const payload = JSON.parse(await readBody(req));
|
|
85
|
+
const lastUser = [...(payload.messages ?? [])].reverse().find((message) => message.role === "user");
|
|
86
|
+
const prompt = messageText(lastUser?.content);
|
|
87
|
+
if (!prompt.trim()) {
|
|
88
|
+
json(res, 400, { error: { message: "messages 中没有 user 内容", type: "invalid_request_error" } });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.onLog?.(`chat request model=${payload.model ?? this.model} stream=${Boolean(payload.stream)}`);
|
|
92
|
+
this.onExchange?.("request", prompt, peerId, agentAid);
|
|
93
|
+
const content = await this.complete(prompt, abort.signal);
|
|
94
|
+
this.onExchange?.("reply", content, peerId, agentAid);
|
|
95
|
+
if (abort.signal.aborted || res.destroyed)
|
|
96
|
+
return;
|
|
97
|
+
const now = Math.floor(Date.now() / 1000);
|
|
98
|
+
const id = `chatcmpl-${(0, node_crypto_1.randomUUID)()}`;
|
|
99
|
+
const model = payload.model ?? this.model;
|
|
100
|
+
if (payload.stream) {
|
|
101
|
+
res.statusCode = 200;
|
|
102
|
+
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
103
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
104
|
+
res.setHeader("Connection", "keep-alive");
|
|
105
|
+
res.write(`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created: now, model, choices: [{ index: 0, delta: { role: "assistant", content }, finish_reason: null }] })}\n\n`);
|
|
106
|
+
res.write(`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created: now, model, choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}\n\n`);
|
|
107
|
+
res.end("data: [DONE]\n\n");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
json(res, 200, {
|
|
111
|
+
id, object: "chat.completion", created: now, model,
|
|
112
|
+
choices: [{ index: 0, message: { role: "assistant", content }, finish_reason: "stop" }],
|
|
113
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
this.onExchange?.("error", error instanceof Error ? error.message : String(error), peerId, agentAid);
|
|
118
|
+
if (abort.signal.aborted || res.destroyed)
|
|
119
|
+
return;
|
|
120
|
+
json(res, 500, { error: { message: error instanceof Error ? error.message : String(error), type: "server_error" } });
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
res.off("close", onClose);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
createServer() {
|
|
127
|
+
return (0, node_http_1.createServer)((req, res) => void this.handle(req, res));
|
|
128
|
+
}
|
|
129
|
+
async listen(port = 17893, host = "127.0.0.1") {
|
|
130
|
+
if (this.server)
|
|
131
|
+
return;
|
|
132
|
+
this.server = this.createServer();
|
|
133
|
+
await new Promise((resolve, reject) => {
|
|
134
|
+
this.server.once("error", reject);
|
|
135
|
+
this.server.listen(port, host, () => resolve());
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
async close() {
|
|
139
|
+
const server = this.server;
|
|
140
|
+
this.server = undefined;
|
|
141
|
+
if (!server)
|
|
142
|
+
return;
|
|
143
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
exports.OpenAIAdapter = OpenAIAdapter;
|
package/package.json
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-bridge-qwenwork",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "agent-bridge 的千问办公插件。",
|
|
5
|
+
"repository": { "type": "git", "url": "https://codeup.aliyun.com/6808da56fb3da14b899afd54/agent_bridge.git", "directory": "packages/qwenwork" },
|
|
6
|
+
"main": "dist/src-ts/plugins/qwenwork/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/",
|
|
9
|
+
"plugin.json",
|
|
10
|
+
"config.schema.json",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"agentBridge": {
|
|
14
|
+
"plugin": "./plugin.json"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"registry": "https://registry.npmjs.org"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/plugin.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "qwenwork",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"displayName": "千问办公",
|
|
5
|
+
"entry": "dist/src-ts/plugins/qwenwork/index.js",
|
|
6
|
+
"apiVersion": "1",
|
|
7
|
+
"process": "in-process",
|
|
8
|
+
"capabilities": ["chat", "openai-compatible", "health"],
|
|
9
|
+
"configSchema": "config.schema.json"
|
|
10
|
+
}
|