@wu529778790/open-im 1.10.3-beta.0 → 1.10.3-beta.2
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/dist/adapters/claude-sdk-adapter.js +2 -1
- package/dist/manager-control.js +27 -6
- package/dist/node-exec.d.ts +6 -0
- package/dist/node-exec.js +16 -0
- package/dist/service-control.js +4 -2
- package/dist/shared/ai-task.js +9 -4
- package/dist/shared/utils.d.ts +5 -0
- package/dist/shared/utils.js +19 -0
- package/dist/telemetry/telemetry-upload.d.ts +4 -1
- package/dist/telemetry/telemetry-upload.js +46 -18
- package/dist/workbuddy/message-sender.js +3 -2
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
13
13
|
import { homedir } from 'os';
|
|
14
14
|
import { join } from 'path';
|
|
15
15
|
import { createLogger } from '../logger.js';
|
|
16
|
+
import { toReplyPlainText } from '../shared/utils.js';
|
|
16
17
|
const log = createLogger('ClaudeSDK');
|
|
17
18
|
function loadUserPluginSettings() {
|
|
18
19
|
try {
|
|
@@ -380,7 +381,7 @@ export class ClaudeSDKAdapter {
|
|
|
380
381
|
callbacks.onError(errMsg);
|
|
381
382
|
return;
|
|
382
383
|
}
|
|
383
|
-
const resultText = m.result ?? '';
|
|
384
|
+
const resultText = toReplyPlainText(m.result ?? '');
|
|
384
385
|
const result = {
|
|
385
386
|
success,
|
|
386
387
|
result: resultText,
|
package/dist/manager-control.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "
|
|
|
3
3
|
import { dirname, extname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { APP_HOME } from "./constants.js";
|
|
6
|
+
import { resolveNodeExecutable } from "./node-exec.js";
|
|
6
7
|
import { isRunning } from "./service-control.js";
|
|
7
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
const PID_FILE = join(APP_HOME, "open-im.pid");
|
|
@@ -12,15 +13,16 @@ function logError(prefix, err) {
|
|
|
12
13
|
process.stderr.write(`[manager-control] ${prefix} ${msg}\n`);
|
|
13
14
|
}
|
|
14
15
|
function getManagerEntry() {
|
|
16
|
+
const node = resolveNodeExecutable();
|
|
15
17
|
const extension = extname(fileURLToPath(import.meta.url));
|
|
16
18
|
if (extension === ".ts") {
|
|
17
19
|
return {
|
|
18
|
-
command:
|
|
20
|
+
command: node,
|
|
19
21
|
args: ["--import", "tsx", join(__dirname, "manager.ts")],
|
|
20
22
|
};
|
|
21
23
|
}
|
|
22
24
|
return {
|
|
23
|
-
command:
|
|
25
|
+
command: node,
|
|
24
26
|
args: [join(__dirname, "manager.js")],
|
|
25
27
|
};
|
|
26
28
|
}
|
|
@@ -95,12 +97,31 @@ export async function startManagerProcess(cwd) {
|
|
|
95
97
|
env: process.env,
|
|
96
98
|
windowsHide: process.platform === "win32",
|
|
97
99
|
});
|
|
98
|
-
|
|
100
|
+
try {
|
|
101
|
+
await new Promise((resolve, reject) => {
|
|
102
|
+
const onSpawn = () => {
|
|
103
|
+
child.off("error", onError);
|
|
104
|
+
resolve();
|
|
105
|
+
};
|
|
106
|
+
const onError = (err) => {
|
|
107
|
+
child.off("spawn", onSpawn);
|
|
108
|
+
reject(err);
|
|
109
|
+
};
|
|
110
|
+
child.once("spawn", onSpawn);
|
|
111
|
+
child.once("error", onError);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
99
115
|
logError("Manager process spawn failed:", err);
|
|
100
|
-
|
|
116
|
+
const hint = " 若 Node 安装路径已失效,请设置环境变量 OPEN_IM_NODE 指向有效的 node 可执行文件(Windows 一般为 node.exe)。";
|
|
117
|
+
throw new Error(`Failed to start manager process: ${err instanceof Error ? err.message : String(err)}${hint}`);
|
|
118
|
+
}
|
|
101
119
|
child.unref();
|
|
102
|
-
|
|
103
|
-
|
|
120
|
+
child.on("error", (err) => {
|
|
121
|
+
logError("Manager process error after spawn:", err);
|
|
122
|
+
});
|
|
123
|
+
if (child.pid === undefined || child.pid === null) {
|
|
124
|
+
throw new Error("Failed to start manager process: no PID after spawn.");
|
|
104
125
|
}
|
|
105
126
|
writeManagerPid(child.pid);
|
|
106
127
|
const deadline = Date.now() + 8000;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* 用于 spawn 子进程的 Node 可执行文件路径。
|
|
4
|
+
* Windows 上偶发 process.execPath 指向已删除/移动的安装(如 D: 盘路径失效),导致 ENOENT;
|
|
5
|
+
* 此时回退到 PATH 中的 `node`,或通过 OPEN_IM_NODE / NODE_EXE 显式指定。
|
|
6
|
+
*/
|
|
7
|
+
export function resolveNodeExecutable() {
|
|
8
|
+
const fromEnv = (process.env.OPEN_IM_NODE ?? process.env.NODE_EXE)?.trim();
|
|
9
|
+
if (fromEnv && existsSync(fromEnv)) {
|
|
10
|
+
return fromEnv;
|
|
11
|
+
}
|
|
12
|
+
if (process.execPath && existsSync(process.execPath)) {
|
|
13
|
+
return process.execPath;
|
|
14
|
+
}
|
|
15
|
+
return "node";
|
|
16
|
+
}
|
package/dist/service-control.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { dirname, extname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { APP_HOME, SHUTDOWN_PORT } from "./constants.js";
|
|
6
|
+
import { resolveNodeExecutable } from "./node-exec.js";
|
|
6
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
const PID_FILE = join(APP_HOME, "open-im-worker.pid");
|
|
8
9
|
const PORT_FILE = join(APP_HOME, "open-im.port");
|
|
@@ -16,15 +17,16 @@ function removePortFile() {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
function getServiceEntry() {
|
|
20
|
+
const node = resolveNodeExecutable();
|
|
19
21
|
const extension = extname(fileURLToPath(import.meta.url));
|
|
20
22
|
if (extension === ".ts") {
|
|
21
23
|
return {
|
|
22
|
-
command:
|
|
24
|
+
command: node,
|
|
23
25
|
args: ["--import", "tsx", join(__dirname, "index.ts")],
|
|
24
26
|
};
|
|
25
27
|
}
|
|
26
28
|
return {
|
|
27
|
-
command:
|
|
29
|
+
command: node,
|
|
28
30
|
args: [join(__dirname, "index.js")],
|
|
29
31
|
};
|
|
30
32
|
}
|
package/dist/shared/ai-task.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 共享 AI 任务执行层,支持多 ToolAdapter。
|
|
3
3
|
*/
|
|
4
4
|
import { resolvePlatformAiCommand } from '../config.js';
|
|
5
|
-
import { formatToolStats, formatToolCallNotification, getContextWarning, getAIToolDisplayName, } from './utils.js';
|
|
5
|
+
import { formatToolStats, formatToolCallNotification, getContextWarning, getAIToolDisplayName, toReplyPlainText, } from './utils.js';
|
|
6
6
|
import { createLogger, emitStructuredEvent } from '../logger.js';
|
|
7
7
|
import { hashUserId } from '../telemetry/hash-user.js';
|
|
8
8
|
import { sanitize } from '../sanitize.js';
|
|
@@ -171,10 +171,11 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
171
171
|
pendingUpdate = null;
|
|
172
172
|
}
|
|
173
173
|
const note = buildCompletionNote(result, sessionManager, ctx);
|
|
174
|
-
const
|
|
174
|
+
const raw = result.accumulated ||
|
|
175
175
|
result.result ||
|
|
176
176
|
taskState.latestContent ||
|
|
177
|
-
'
|
|
177
|
+
'';
|
|
178
|
+
const output = raw ? toReplyPlainText(raw) : '(无输出)';
|
|
178
179
|
if (!result.accumulated && !result.result && taskState.latestContent) {
|
|
179
180
|
log.warn(`Empty AI output from adapter but had streamed content (${taskState.latestContent.length} chars), using latestContent. platform=${ctx.platform}, taskKey=${ctx.taskKey}`);
|
|
180
181
|
}
|
|
@@ -287,7 +288,11 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
287
288
|
durationMs: 0,
|
|
288
289
|
errorSnippet: sanitize(String(err).slice(0, 400)),
|
|
289
290
|
});
|
|
290
|
-
platformAdapter
|
|
291
|
+
platformAdapter
|
|
292
|
+
.sendError(`内部错误:${err instanceof Error ? err.message : String(err)}`)
|
|
293
|
+
.catch(() => {
|
|
294
|
+
/* ignore */
|
|
295
|
+
});
|
|
291
296
|
resolve();
|
|
292
297
|
}
|
|
293
298
|
return;
|
package/dist/shared/utils.d.ts
CHANGED
|
@@ -13,6 +13,11 @@ export declare function handleEnqueueResult(enqueueResult: EnqueueResult, sendTe
|
|
|
13
13
|
queueFull?: string;
|
|
14
14
|
queued?: string;
|
|
15
15
|
}): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* 将 AI 适配器可能返回的非字符串(如第三方 API 把 result 解析成对象)转为可展示的文本,
|
|
18
|
+
* 避免 IM 侧出现 "[object Object]"。
|
|
19
|
+
*/
|
|
20
|
+
export declare function toReplyPlainText(value: unknown): string;
|
|
16
21
|
/** 转义路径供 Markdown 显示,防止 xxx.yyy.com 被解析为链接 */
|
|
17
22
|
export declare function escapePathForMarkdown(path: string): string;
|
|
18
23
|
/** AI 工具显示名称映射(aiCommand -> 用户友好名称) */
|
package/dist/shared/utils.js
CHANGED
|
@@ -16,6 +16,25 @@ export async function handleEnqueueResult(enqueueResult, sendTextReply, messages
|
|
|
16
16
|
await sendTextReply(messages?.queued ?? DEFAULT_QUEUED_MESSAGE);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* 将 AI 适配器可能返回的非字符串(如第三方 API 把 result 解析成对象)转为可展示的文本,
|
|
21
|
+
* 避免 IM 侧出现 "[object Object]"。
|
|
22
|
+
*/
|
|
23
|
+
export function toReplyPlainText(value) {
|
|
24
|
+
if (value == null)
|
|
25
|
+
return '';
|
|
26
|
+
if (typeof value === 'string')
|
|
27
|
+
return value;
|
|
28
|
+
if (typeof value === 'object') {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.stringify(value, null, 2);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return String(value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return String(value);
|
|
37
|
+
}
|
|
19
38
|
/** 转义路径供 Markdown 显示,防止 xxx.yyy.com 被解析为链接 */
|
|
20
39
|
export function escapePathForMarkdown(path) {
|
|
21
40
|
return `\`${path.replace(/`/g, '\\`')}\``;
|
|
@@ -3,6 +3,9 @@ export declare function initTelemetryUpload(opts: {
|
|
|
3
3
|
url?: string;
|
|
4
4
|
token?: string;
|
|
5
5
|
}): void;
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* 单行 NDJSON(已含 \\n)。
|
|
8
|
+
* 满 BATCH_MAX_LINES 立即上传;否则自「当前积压周期」起至少间隔 MIN_PARTIAL_FLUSH_INTERVAL_MS 再上传。
|
|
9
|
+
*/
|
|
7
10
|
export declare function enqueueTelemetryLine(line: string): void;
|
|
8
11
|
export declare function shutdownTelemetryUpload(): Promise<void>;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 遥测 NDJSON 上传:
|
|
3
|
+
* - 单次 POST 最多 BATCH_MAX_LINES 条(控制 body 大小);
|
|
4
|
+
* - 事件稀疏时按 MIN_PARTIAL_FLUSH_INTERVAL_MS 合并上报,降低时间维度上的请求频率;
|
|
5
|
+
* - 积压达到 BATCH_MAX_LINES 时仍立即上传(突发流量)。
|
|
6
|
+
*/
|
|
7
|
+
const BATCH_MAX_LINES = 100;
|
|
8
|
+
/** 稀疏流量:队列未满批时,最早在「首条入队」后经过该间隔才上传(避免短间隔反复 POST) */
|
|
9
|
+
const MIN_PARTIAL_FLUSH_INTERVAL_MS = 60_000;
|
|
3
10
|
const MAX_QUEUE = 8000;
|
|
4
11
|
const MAX_BACKOFF_MS = 120_000;
|
|
5
12
|
const INITIAL_BACKOFF_MS = 1000;
|
|
@@ -23,13 +30,15 @@ function clearBackoffTimer() {
|
|
|
23
30
|
backoffTimer = null;
|
|
24
31
|
}
|
|
25
32
|
}
|
|
26
|
-
function
|
|
27
|
-
if (!uploadEnabled || !endpoint || idleTimer || flushing)
|
|
33
|
+
function schedulePartialFlush() {
|
|
34
|
+
if (!uploadEnabled || !endpoint || idleTimer || flushing || backoffTimer)
|
|
28
35
|
return;
|
|
29
36
|
idleTimer = setTimeout(() => {
|
|
30
37
|
idleTimer = null;
|
|
31
|
-
void flushPipeline()
|
|
32
|
-
|
|
38
|
+
void flushPipeline().catch(() => {
|
|
39
|
+
/* 静默;退避重试由 flushPipeline/backoff 处理 */
|
|
40
|
+
});
|
|
41
|
+
}, MIN_PARTIAL_FLUSH_INTERVAL_MS);
|
|
33
42
|
}
|
|
34
43
|
async function postBatch(lines) {
|
|
35
44
|
if (!endpoint || lines.length === 0)
|
|
@@ -41,8 +50,19 @@ async function postBatch(lines) {
|
|
|
41
50
|
};
|
|
42
51
|
if (bearer)
|
|
43
52
|
headers.authorization = `Bearer ${bearer}`;
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch(endpoint, { method: 'POST', headers, body });
|
|
55
|
+
try {
|
|
56
|
+
await res.text();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
/* ignore body read errors */
|
|
60
|
+
}
|
|
61
|
+
return res.ok;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
46
66
|
}
|
|
47
67
|
async function flushPipeline() {
|
|
48
68
|
if (!uploadEnabled || !endpoint || flushing)
|
|
@@ -72,11 +92,11 @@ async function flushPipeline() {
|
|
|
72
92
|
}
|
|
73
93
|
}
|
|
74
94
|
}
|
|
95
|
+
catch {
|
|
96
|
+
/* 静默:上传失败不得向外抛,避免 unhandledRejection */
|
|
97
|
+
}
|
|
75
98
|
finally {
|
|
76
99
|
flushing = false;
|
|
77
|
-
if (uploadEnabled && endpoint && queue.length > 0 && !backoffTimer) {
|
|
78
|
-
scheduleIdleFlush();
|
|
79
|
-
}
|
|
80
100
|
}
|
|
81
101
|
}
|
|
82
102
|
function backoffThenRetry() {
|
|
@@ -85,7 +105,7 @@ function backoffThenRetry() {
|
|
|
85
105
|
backoffTimer = setTimeout(() => {
|
|
86
106
|
backoffTimer = null;
|
|
87
107
|
backoffMs = Math.min(MAX_BACKOFF_MS, backoffMs * 2);
|
|
88
|
-
void flushPipeline().finally(resolve);
|
|
108
|
+
void flushPipeline().catch(() => { }).finally(resolve);
|
|
89
109
|
}, backoffMs);
|
|
90
110
|
});
|
|
91
111
|
}
|
|
@@ -100,20 +120,27 @@ export function initTelemetryUpload(opts) {
|
|
|
100
120
|
queue = [];
|
|
101
121
|
}
|
|
102
122
|
}
|
|
103
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* 单行 NDJSON(已含 \\n)。
|
|
125
|
+
* 满 BATCH_MAX_LINES 立即上传;否则自「当前积压周期」起至少间隔 MIN_PARTIAL_FLUSH_INTERVAL_MS 再上传。
|
|
126
|
+
*/
|
|
104
127
|
export function enqueueTelemetryLine(line) {
|
|
105
128
|
if (!uploadEnabled || !endpoint)
|
|
106
129
|
return;
|
|
107
130
|
if (queue.length >= MAX_QUEUE) {
|
|
108
131
|
queue.splice(0, Math.floor(MAX_QUEUE / 4));
|
|
109
132
|
}
|
|
133
|
+
const wasEmpty = queue.length === 0;
|
|
110
134
|
queue.push(line);
|
|
111
135
|
if (queue.length >= BATCH_MAX_LINES) {
|
|
112
136
|
clearIdleTimer();
|
|
113
|
-
void flushPipeline()
|
|
137
|
+
void flushPipeline().catch(() => {
|
|
138
|
+
/* 静默 */
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
114
141
|
}
|
|
115
|
-
|
|
116
|
-
|
|
142
|
+
if (wasEmpty && !idleTimer && !flushing && !backoffTimer) {
|
|
143
|
+
schedulePartialFlush();
|
|
117
144
|
}
|
|
118
145
|
}
|
|
119
146
|
export async function shutdownTelemetryUpload() {
|
|
@@ -144,10 +171,11 @@ export async function shutdownTelemetryUpload() {
|
|
|
144
171
|
};
|
|
145
172
|
if (br)
|
|
146
173
|
headers.authorization = `Bearer ${br}`;
|
|
147
|
-
await fetch(ep, { method: 'POST', headers, body });
|
|
174
|
+
const res = await fetch(ep, { method: 'POST', headers, body });
|
|
175
|
+
await res.text().catch(() => { });
|
|
148
176
|
}
|
|
149
177
|
catch {
|
|
150
|
-
/* best effort */
|
|
178
|
+
/* best effort,静默 */
|
|
151
179
|
}
|
|
152
180
|
}
|
|
153
181
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* WorkBuddy Message Sender - Send responses to WeChat KF
|
|
3
3
|
*/
|
|
4
4
|
import { createLogger } from '../logger.js';
|
|
5
|
+
import { toReplyPlainText } from '../shared/utils.js';
|
|
5
6
|
import { getCentrifugeClient } from './client.js';
|
|
6
7
|
const log = createLogger('WorkBuddySender');
|
|
7
8
|
/**
|
|
@@ -17,7 +18,7 @@ export async function sendTextReply(_client, chatId, text, msgId) {
|
|
|
17
18
|
await client.sendPromptResponse({
|
|
18
19
|
session_id: chatId,
|
|
19
20
|
prompt_id: msgId,
|
|
20
|
-
content: [{ type: 'text', text }],
|
|
21
|
+
content: [{ type: 'text', text: toReplyPlainText(text) }],
|
|
21
22
|
stop_reason: 'end_turn',
|
|
22
23
|
});
|
|
23
24
|
}
|
|
@@ -47,5 +48,5 @@ export function sendStreamingChunk(_client, chatId, text, msgId) {
|
|
|
47
48
|
log.warn('WorkBuddy client not available, cannot send chunk');
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
50
|
-
client.sendMessageChunk(chatId, msgId, { type: 'text', text });
|
|
51
|
+
client.sendMessageChunk(chatId, msgId, { type: 'text', text: toReplyPlainText(text) });
|
|
51
52
|
}
|