@rezti/dsh-rez-wechat 0.1.8 → 0.1.12
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 +17 -8
- package/cordis.patch.yml +5 -0
- package/lib/channel.d.ts +4 -0
- package/lib/channel.js +46 -4
- package/lib/index.js +34 -3
- package/lib/unarchive.d.ts +43 -0
- package/lib/unarchive.js +115 -0
- package/lib/web-shim.d.ts +40 -6
- package/lib/web-shim.js +598 -38
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
# @rezti/dsh-rez-wechat
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
企业微信走现成 `wecom-mcp`;个人微信走 QClaw 同款 `dsh-wechat-bridge`(腾讯 iLink)。都不自写长连接。
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
dsh plugin --profile web add @rezti/dsh-rez-suite
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
以后升级只记:`dsh plugin --profile web update @rezti/dsh-rez-suite`。不要再单独 add 本包。
|
|
4
10
|
|
|
5
11
|
| 能力 | 做法 |
|
|
6
12
|
|---|---|
|
|
7
|
-
| 个人微信(QClaw / ClawBot) | 打开 dsh Web → Rez 面板 → **微信** → 扫码。之后直接在微信里说话。消息进左侧 **微信** 文件夹的同一会话,模型跟网页默认。 |
|
|
8
13
|
| 企业微信发到群 | `wecom-mcp`(`mcp__wechat__*`)+ `REZ_WECHAT_WEBHOOK` |
|
|
14
|
+
| 个人微信(QClaw / ClawBot) | dsh Web 设置面板扫码,之后直接在微信里说话。消息进左侧 **微信** 文件夹的同一会话,模型跟网页默认。 |
|
|
9
15
|
|
|
10
|
-
|
|
16
|
+
个人微信桥接到正在跑的 `dsh web`(不是 `dsh --profile headless`):
|
|
11
17
|
|
|
12
18
|
- 左侧工作区分组是 **微信**,不是 Ungrouped
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
19
|
+
- 连续消息延续同一会话
|
|
20
|
+
- 模型跟网页默认(网页选了 Kimi,微信也走 Kimi)
|
|
21
|
+
- 网页归档后再从微信说话会自动回到侧栏(官方会藏空白会话,所以「重启对话」会先打一句招呼)
|
|
22
|
+
- 网页端选择题(Selection)会编成 `1. 2. 3.` 发到微信;回复数字或选项原文即可继续,不必在网页点选
|
|
23
|
+
- 思考过程不会发到微信;多段正文会保留换行
|
|
24
|
+
- 通道断了点「重新连接」,已登录不必再扫码
|
|
16
25
|
|
|
17
|
-
|
|
26
|
+
扫码 UI 在 suite 面板;通道本身在本包。不要对同一 profile 既 add suite 又 add wechat,会重复注册。
|
|
18
27
|
|
|
19
|
-
这是 ClawBot
|
|
28
|
+
这是 ClawBot 客服会话,不是用个人微信号管好友/群,也不是客户层 WhatsApp/邮件通道。
|
package/cordis.patch.yml
ADDED
package/lib/channel.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface WeixinStatus {
|
|
|
11
11
|
qrDataUrl?: string;
|
|
12
12
|
hint: string;
|
|
13
13
|
}
|
|
14
|
+
export declare function weixinLoginKind(authExists: boolean): 'reconnect' | 'qr';
|
|
14
15
|
export declare function weixinRunEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
15
16
|
export declare class WeixinChannel {
|
|
16
17
|
private loginChild;
|
|
@@ -20,6 +21,9 @@ export declare class WeixinChannel {
|
|
|
20
21
|
private qrDataUrl;
|
|
21
22
|
private hint;
|
|
22
23
|
private logBuf;
|
|
24
|
+
private runStopped;
|
|
25
|
+
private runExits;
|
|
26
|
+
private runTimer;
|
|
23
27
|
status(): WeixinStatus;
|
|
24
28
|
startIfLoggedIn(): void;
|
|
25
29
|
startLogin(): Promise<WeixinStatus>;
|
package/lib/channel.js
CHANGED
|
@@ -11,6 +11,9 @@ import { weixinBridgeEnv, writeWeixinDshLauncher } from './web-shim.js';
|
|
|
11
11
|
const DATA_DIR = join(homedir(), '.dsh', 'dsh-rez-weixin');
|
|
12
12
|
const AUTH_FILE = join(DATA_DIR, 'weixin-auth.json');
|
|
13
13
|
const SHIM_SCRIPT = join(dirname(fileURLToPath(import.meta.url)), 'web-shim-cli.js');
|
|
14
|
+
export function weixinLoginKind(authExists) {
|
|
15
|
+
return authExists ? 'reconnect' : 'qr';
|
|
16
|
+
}
|
|
14
17
|
export function weixinRunEnv(env = process.env) {
|
|
15
18
|
const launcher = writeWeixinDshLauncher({
|
|
16
19
|
dataDir: DATA_DIR,
|
|
@@ -49,6 +52,9 @@ export class WeixinChannel {
|
|
|
49
52
|
qrDataUrl;
|
|
50
53
|
hint = '打开 Rez 面板的「微信」页扫码,然后直接在微信里说话。';
|
|
51
54
|
logBuf = '';
|
|
55
|
+
runStopped = true;
|
|
56
|
+
runExits = 0;
|
|
57
|
+
runTimer;
|
|
52
58
|
status() {
|
|
53
59
|
const loggedIn = existsSync(AUTH_FILE);
|
|
54
60
|
const status = {
|
|
@@ -69,10 +75,16 @@ export class WeixinChannel {
|
|
|
69
75
|
}
|
|
70
76
|
async startLogin() {
|
|
71
77
|
this.stopLogin();
|
|
72
|
-
this.
|
|
78
|
+
this.stopRun();
|
|
73
79
|
this.qrContent = undefined;
|
|
74
80
|
this.qrDataUrl = undefined;
|
|
75
81
|
this.logBuf = '';
|
|
82
|
+
if (weixinLoginKind(existsSync(AUTH_FILE)) === 'reconnect') {
|
|
83
|
+
this.hint = '已重新拉起微信监听。若手机上仍发不出去,先点退出再扫码。';
|
|
84
|
+
this.startRun();
|
|
85
|
+
return this.status();
|
|
86
|
+
}
|
|
87
|
+
this.phase = 'login';
|
|
76
88
|
this.hint = '正在向微信申请二维码…';
|
|
77
89
|
const child = spawn('npx', ['-y', 'dsh-wechat-bridge', 'login', '--data-dir', DATA_DIR], {
|
|
78
90
|
env: process.env,
|
|
@@ -147,6 +159,11 @@ export class WeixinChannel {
|
|
|
147
159
|
this.stopRun();
|
|
148
160
|
}
|
|
149
161
|
startRun() {
|
|
162
|
+
this.runStopped = false;
|
|
163
|
+
if (this.runTimer !== undefined) {
|
|
164
|
+
clearTimeout(this.runTimer);
|
|
165
|
+
this.runTimer = undefined;
|
|
166
|
+
}
|
|
150
167
|
if (this.runChild !== undefined && this.runChild.exitCode === null)
|
|
151
168
|
return;
|
|
152
169
|
this.phase = 'listening';
|
|
@@ -156,13 +173,32 @@ export class WeixinChannel {
|
|
|
156
173
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
157
174
|
});
|
|
158
175
|
this.runChild = child;
|
|
176
|
+
const startedAt = Date.now();
|
|
159
177
|
child.on('exit', code => {
|
|
160
178
|
if (this.runChild === child)
|
|
161
179
|
this.runChild = undefined;
|
|
162
|
-
if (this.
|
|
163
|
-
|
|
164
|
-
|
|
180
|
+
if (this.runStopped)
|
|
181
|
+
return;
|
|
182
|
+
if (Date.now() - startedAt > 30_000)
|
|
183
|
+
this.runExits = 0;
|
|
184
|
+
this.runExits += 1;
|
|
185
|
+
if (!existsSync(AUTH_FILE)) {
|
|
186
|
+
this.phase = 'error';
|
|
187
|
+
this.hint = '微信登录已失效。请重新扫码。';
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (this.runExits >= 5) {
|
|
191
|
+
this.phase = 'error';
|
|
192
|
+
this.hint = '微信监听反复断开。请点退出,再扫码登录。';
|
|
193
|
+
return;
|
|
165
194
|
}
|
|
195
|
+
this.phase = 'idle';
|
|
196
|
+
this.hint = `微信监听断了(退出码 ${code ?? 'null'}),正在重连…`;
|
|
197
|
+
this.runTimer = setTimeout(() => {
|
|
198
|
+
this.runTimer = undefined;
|
|
199
|
+
if (!this.runStopped)
|
|
200
|
+
this.startRun();
|
|
201
|
+
}, 2000);
|
|
166
202
|
});
|
|
167
203
|
}
|
|
168
204
|
stopLogin() {
|
|
@@ -172,6 +208,12 @@ export class WeixinChannel {
|
|
|
172
208
|
this.loginChild = undefined;
|
|
173
209
|
}
|
|
174
210
|
stopRun() {
|
|
211
|
+
this.runStopped = true;
|
|
212
|
+
this.runExits = 0;
|
|
213
|
+
if (this.runTimer !== undefined) {
|
|
214
|
+
clearTimeout(this.runTimer);
|
|
215
|
+
this.runTimer = undefined;
|
|
216
|
+
}
|
|
175
217
|
if (this.runChild === undefined)
|
|
176
218
|
return;
|
|
177
219
|
this.runChild.kill('SIGTERM');
|
package/lib/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { startMcpWhenSecret } from '@rezti/dsh-rez-sso';
|
|
2
2
|
import { WeixinChannel } from './channel.js';
|
|
3
|
+
import { revealSessionInRegistry } from './unarchive.js';
|
|
3
4
|
export const name = 'rez-wechat';
|
|
4
5
|
export const inject = ['systemPrompt', 'webServer', 'rezSso'];
|
|
5
6
|
const GUIDANCE = [
|
|
6
7
|
'企业微信群发送走官方 dsh-mcp-client + wecom-mcp(mcp__wechat__*),密钥 REZ_WECHAT_WEBHOOK。未配置时不拉起 wecom-mcp。Webhook 在 设置 → 插件 → ReZ-TI 里填。',
|
|
7
|
-
'个人微信是 QClaw 同款 ClawBot 通道:在 Rez 面板「微信」页扫码一次,然后直接在微信里说话。消息进入网页左侧「微信」文件夹并延续同一会话,模型跟网页默认(在网页里选了 Kimi 就会走 Kimi
|
|
8
|
+
'个人微信是 QClaw 同款 ClawBot 通道:在 Rez 面板「微信」页扫码一次,然后直接在微信里说话。消息进入网页左侧「微信」文件夹并延续同一会话,模型跟网页默认(在网页里选了 Kimi 就会走 Kimi)。网页端归档了也能从微信自动恢复到侧栏(空白会话网页会藏起来,所以新开的会话会先打一句招呼再出现)。网页端出现选择题时会编成纯文本发到微信,回复数字或选项原文即可;思考过程不会转发。发「重启对话」会新开一条可见的网页会话并把旧的归档。通道断了点「重新连接」(已登录不必再扫码)。不要再跑 npx weixin-mcp login,也不要调用 mcp__weixin__*。网页端必须开着。',
|
|
8
9
|
'这是 ClawBot 客服会话,不是用个人微信号管好友/群。对外发消息必须先征得用户批准。',
|
|
9
10
|
].join('');
|
|
10
11
|
function isLoopbackRequest(request) {
|
|
@@ -43,7 +44,7 @@ async function readJsonBody(req) {
|
|
|
43
44
|
return undefined;
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
|
-
function weixinRoutes(channel) {
|
|
47
|
+
function weixinRoutes(channel, ctx) {
|
|
47
48
|
const path = '/api/dsh-rez-suite/weixin';
|
|
48
49
|
const guard = (req, res, method) => {
|
|
49
50
|
if (!isLoopbackRequest(req)) {
|
|
@@ -93,8 +94,38 @@ function weixinRoutes(channel) {
|
|
|
93
94
|
writeJson(res, 200, channel.sendVerify(code));
|
|
94
95
|
},
|
|
95
96
|
},
|
|
97
|
+
{
|
|
98
|
+
kind: 'exact',
|
|
99
|
+
path: path + '/unarchive',
|
|
100
|
+
handler: async (req, res) => {
|
|
101
|
+
if (!guard(req, res, 'POST'))
|
|
102
|
+
return;
|
|
103
|
+
const body = await readJsonBody(req);
|
|
104
|
+
const sessionId = typeof body?.sessionId === 'string' ? body.sessionId.trim() : '';
|
|
105
|
+
if (sessionId.length === 0) {
|
|
106
|
+
writeJson(res, 400, { error: 'missing sessionId' });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const workspacePath = typeof body?.workspacePath === 'string' ? body.workspacePath.trim() : '';
|
|
110
|
+
try {
|
|
111
|
+
writeJson(res, 200, await revealSessionInRegistry(getWorkspaceRegistry(ctx), sessionId, workspacePath.length > 0 ? workspacePath : undefined));
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
115
|
+
writeJson(res, 501, { error: message });
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
},
|
|
96
119
|
];
|
|
97
120
|
}
|
|
121
|
+
function getWorkspaceRegistry(ctx) {
|
|
122
|
+
try {
|
|
123
|
+
return ctx.get('workspaceRegistry');
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
98
129
|
export async function apply(ctx) {
|
|
99
130
|
const channel = new WeixinChannel();
|
|
100
131
|
channel.startIfLoggedIn();
|
|
@@ -103,7 +134,7 @@ export async function apply(ctx) {
|
|
|
103
134
|
order: 162,
|
|
104
135
|
text: GUIDANCE,
|
|
105
136
|
}), 'dsh-rez-wechat: prompt');
|
|
106
|
-
const routes = weixinRoutes(channel);
|
|
137
|
+
const routes = weixinRoutes(channel, ctx);
|
|
107
138
|
ctx.effect(() => {
|
|
108
139
|
const disposers = routes.map(route => ctx.webServer.register(route));
|
|
109
140
|
return () => { for (const dispose of disposers)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh 0.1.0-rc.6 archives one-way over RPC. Stock Harness still exposes
|
|
3
|
+
* workspaceRegistry.requireState / setState (same primitives archiveSession
|
|
4
|
+
* uses). Weixin follow-ups call this so an archived web session reappears
|
|
5
|
+
* in the sidebar instead of staying hidden while WeChat keeps talking to it.
|
|
6
|
+
*
|
|
7
|
+
* Disk form is often `global.archivedSessionIds` in workspace.json; the live
|
|
8
|
+
* registry state may be that nested object or a flat `{ archivedSessionIds }`.
|
|
9
|
+
* After dropping the id, attachSession puts Ungrouped sessions into 微信 —
|
|
10
|
+
* session.create({ cwd }) never joins a workspace on its own after bootstrap.
|
|
11
|
+
*/
|
|
12
|
+
export interface WorkspaceLike {
|
|
13
|
+
id?: string;
|
|
14
|
+
path?: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
attachSession?: (sessionId: string) => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export interface WorkspaceRegistryLike {
|
|
19
|
+
requireState?: () => Record<string, unknown>;
|
|
20
|
+
setState?: (state: Record<string, unknown>) => Promise<void>;
|
|
21
|
+
archiveSession?: (sessionId: string) => Promise<void>;
|
|
22
|
+
unarchiveSession?: (sessionId: string) => Promise<void>;
|
|
23
|
+
list?: () => WorkspaceLike[];
|
|
24
|
+
get?: (id: string) => WorkspaceLike | undefined;
|
|
25
|
+
resolveByPath?: (path: string) => Promise<WorkspaceLike | undefined>;
|
|
26
|
+
}
|
|
27
|
+
/** Read archive ids from either the live registry shape or the on-disk global bag. */
|
|
28
|
+
export declare function archivedSessionIds(state: unknown): string[];
|
|
29
|
+
export declare function unarchiveSessionInRegistry(registry: WorkspaceRegistryLike | undefined, sessionId: string): Promise<{
|
|
30
|
+
sessionId: string;
|
|
31
|
+
archived: boolean;
|
|
32
|
+
changed: boolean;
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Drop the archive bit, then attach so the GUI's 微信 folder actually lists it.
|
|
36
|
+
* attachSession is host-only (no workspace.attachSession RPC on stock dsh).
|
|
37
|
+
*/
|
|
38
|
+
export declare function revealSessionInRegistry(registry: WorkspaceRegistryLike | undefined, sessionId: string, workspacePath?: string): Promise<{
|
|
39
|
+
sessionId: string;
|
|
40
|
+
archived: boolean;
|
|
41
|
+
changed: boolean;
|
|
42
|
+
attached: boolean;
|
|
43
|
+
}>;
|
package/lib/unarchive.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh 0.1.0-rc.6 archives one-way over RPC. Stock Harness still exposes
|
|
3
|
+
* workspaceRegistry.requireState / setState (same primitives archiveSession
|
|
4
|
+
* uses). Weixin follow-ups call this so an archived web session reappears
|
|
5
|
+
* in the sidebar instead of staying hidden while WeChat keeps talking to it.
|
|
6
|
+
*
|
|
7
|
+
* Disk form is often `global.archivedSessionIds` in workspace.json; the live
|
|
8
|
+
* registry state may be that nested object or a flat `{ archivedSessionIds }`.
|
|
9
|
+
* After dropping the id, attachSession puts Ungrouped sessions into 微信 —
|
|
10
|
+
* session.create({ cwd }) never joins a workspace on its own after bootstrap.
|
|
11
|
+
*/
|
|
12
|
+
function asRecord(value) {
|
|
13
|
+
return typeof value === 'object' && value !== null ? value : undefined;
|
|
14
|
+
}
|
|
15
|
+
function stringIds(value) {
|
|
16
|
+
if (!Array.isArray(value))
|
|
17
|
+
return [];
|
|
18
|
+
return value.filter((id) => typeof id === 'string');
|
|
19
|
+
}
|
|
20
|
+
/** Read archive ids from either the live registry shape or the on-disk global bag. */
|
|
21
|
+
export function archivedSessionIds(state) {
|
|
22
|
+
const rec = asRecord(state);
|
|
23
|
+
if (rec === undefined)
|
|
24
|
+
return [];
|
|
25
|
+
const top = stringIds(rec.archivedSessionIds);
|
|
26
|
+
if (top.length > 0)
|
|
27
|
+
return top;
|
|
28
|
+
return stringIds(asRecord(rec.global)?.archivedSessionIds);
|
|
29
|
+
}
|
|
30
|
+
function dropArchivedId(state, sessionId) {
|
|
31
|
+
const next = { ...state };
|
|
32
|
+
if (Array.isArray(state.archivedSessionIds)) {
|
|
33
|
+
next.archivedSessionIds = stringIds(state.archivedSessionIds).filter(id => id !== sessionId);
|
|
34
|
+
}
|
|
35
|
+
const global = asRecord(state.global);
|
|
36
|
+
if (global !== undefined && Array.isArray(global.archivedSessionIds)) {
|
|
37
|
+
next.global = {
|
|
38
|
+
...global,
|
|
39
|
+
archivedSessionIds: stringIds(global.archivedSessionIds).filter(id => id !== sessionId),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return next;
|
|
43
|
+
}
|
|
44
|
+
export async function unarchiveSessionInRegistry(registry, sessionId) {
|
|
45
|
+
if (registry !== undefined && typeof registry.unarchiveSession === 'function') {
|
|
46
|
+
await registry.unarchiveSession(sessionId);
|
|
47
|
+
return { sessionId, archived: false, changed: true };
|
|
48
|
+
}
|
|
49
|
+
if (registry === undefined || typeof registry.requireState !== 'function' || typeof registry.setState !== 'function') {
|
|
50
|
+
throw new Error('当前 Harness 不支持取消归档(缺少 workspaceRegistry 状态原语)');
|
|
51
|
+
}
|
|
52
|
+
const state = registry.requireState();
|
|
53
|
+
const ids = archivedSessionIds(state);
|
|
54
|
+
if (!ids.includes(sessionId))
|
|
55
|
+
return { sessionId, archived: false, changed: false };
|
|
56
|
+
await registry.setState(dropArchivedId(state, sessionId));
|
|
57
|
+
return { sessionId, archived: false, changed: true };
|
|
58
|
+
}
|
|
59
|
+
function pathLooksLike(candidate, wanted) {
|
|
60
|
+
if (candidate === undefined || candidate.length === 0)
|
|
61
|
+
return false;
|
|
62
|
+
const a = candidate.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
63
|
+
const b = wanted.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
64
|
+
return a === b || a.endsWith('/' + b.split('/').pop());
|
|
65
|
+
}
|
|
66
|
+
async function attachToWorkspace(workspace, sessionId) {
|
|
67
|
+
if (workspace === undefined || typeof workspace.attachSession !== 'function')
|
|
68
|
+
return false;
|
|
69
|
+
try {
|
|
70
|
+
await workspace.attachSession(sessionId);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Drop the archive bit, then attach so the GUI's 微信 folder actually lists it.
|
|
79
|
+
* attachSession is host-only (no workspace.attachSession RPC on stock dsh).
|
|
80
|
+
*/
|
|
81
|
+
export async function revealSessionInRegistry(registry, sessionId, workspacePath) {
|
|
82
|
+
let unarchived = { sessionId, archived: true, changed: false };
|
|
83
|
+
try {
|
|
84
|
+
unarchived = await unarchiveSessionInRegistry(registry, sessionId);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Stock registry may lack requireState; attachSession still puts the row in 微信.
|
|
88
|
+
}
|
|
89
|
+
let attached = false;
|
|
90
|
+
if (registry !== undefined && workspacePath !== undefined && workspacePath.length > 0 && typeof registry.resolveByPath === 'function') {
|
|
91
|
+
try {
|
|
92
|
+
attached = await attachToWorkspace(await registry.resolveByPath(workspacePath), sessionId);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
attached = false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!attached && registry !== undefined && typeof registry.list === 'function') {
|
|
99
|
+
const workspaces = registry.list();
|
|
100
|
+
const preferred = workspacePath !== undefined
|
|
101
|
+
? workspaces.find(ws => pathLooksLike(ws.path, workspacePath) || ws.title === '微信')
|
|
102
|
+
: undefined;
|
|
103
|
+
if (preferred !== undefined)
|
|
104
|
+
attached = await attachToWorkspace(preferred, sessionId);
|
|
105
|
+
if (!attached) {
|
|
106
|
+
for (const workspace of workspaces) {
|
|
107
|
+
if (await attachToWorkspace(workspace, sessionId)) {
|
|
108
|
+
attached = true;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { ...unarchived, attached };
|
|
115
|
+
}
|
package/lib/web-shim.d.ts
CHANGED
|
@@ -13,12 +13,21 @@ export interface ModelRef {
|
|
|
13
13
|
provider?: string;
|
|
14
14
|
model: string;
|
|
15
15
|
}
|
|
16
|
+
export interface PendingQuestion {
|
|
17
|
+
id: string;
|
|
18
|
+
header?: string;
|
|
19
|
+
question: string;
|
|
20
|
+
options: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface SessionRecord {
|
|
23
|
+
sessionId: string;
|
|
24
|
+
updatedAt: number;
|
|
25
|
+
pendingQuestions?: PendingQuestion[];
|
|
26
|
+
}
|
|
16
27
|
export interface SessionStore {
|
|
17
|
-
sessions: Record<string,
|
|
18
|
-
sessionId: string;
|
|
19
|
-
updatedAt: number;
|
|
20
|
-
}>;
|
|
28
|
+
sessions: Record<string, SessionRecord>;
|
|
21
29
|
}
|
|
30
|
+
export declare const ASK_USER_QUESTION_TOOL = "ask_user_question";
|
|
22
31
|
export interface RpcFn {
|
|
23
32
|
(method: string, params: Record<string, unknown>): Promise<unknown>;
|
|
24
33
|
}
|
|
@@ -28,6 +37,19 @@ export declare function parseHeadlessArgv(argv: string[]): {
|
|
|
28
37
|
error: string;
|
|
29
38
|
};
|
|
30
39
|
export declare function isFreshBridgeTurn(task: string): boolean;
|
|
40
|
+
/** WeChat /new is handled by dsh-wechat-bridge. Chinese "重启对话" is a normal message and must be detected here. */
|
|
41
|
+
export declare function isRestartCommand(text: string): boolean;
|
|
42
|
+
export declare const RESTART_ACK = "\u5DF2\u65B0\u5F00\u7F51\u9875\u4F1A\u8BDD\uFF08\u5DE6\u4FA7\u300C\u5FAE\u4FE1\u300D\u6587\u4EF6\u5939\uFF09\u3002\u4E4B\u540E\u76F4\u63A5\u8BF4\u8BDD\u5373\u53EF\u3002\u518D\u53D1\u300C\u91CD\u542F\u5BF9\u8BDD\u300D\u4F1A\u518D\u5F00\u4E00\u6761\uFF1B\u65E7\u4F1A\u8BDD\u4F1A\u5F52\u6863\uFF0C\u7F51\u9875\u4FA7\u680F\u66F4\u5E72\u51C0\u3002";
|
|
43
|
+
export declare const RESTART_SEED = "\u65B0\u4F1A\u8BDD\u5DF2\u5F00\u59CB\u3002\u8BF7\u7528\u4E00\u53E5\u4E2D\u6587\u6253\u62DB\u547C\uFF0C\u4E0D\u8981\u89E3\u91CA\u5185\u90E8\u6B65\u9AA4\u3002";
|
|
44
|
+
export declare const STUCK_ACK = "\u7F51\u9875\u7AEF\u8FD9\u6761\u4F1A\u8BDD\u5361\u4F4F\u4E86\uFF08\u5728\u7B49\u6279\u51C6\uFF0C\u6216\u6A21\u578B\u628A\u5185\u90E8\u63A8\u7406\u53D1\u51FA\u6765\u4E86\uFF09\u3002\u8BF7\u770B\u7F51\u9875\u5DE6\u4FA7\u300C\u5FAE\u4FE1\u300D\u6587\u4EF6\u5939\uFF0C\u6216\u53D1\u300C\u91CD\u542F\u5BF9\u8BDD\u300D\u3002";
|
|
45
|
+
export declare const RPC_TIMEOUT_MS = 15000;
|
|
46
|
+
export declare const QUESTION_HINT = "\u56DE\u590D\u6570\u5B57\u6216\u9009\u9879\u539F\u6587\u5373\u53EF\u3002";
|
|
47
|
+
export declare function stripThinkTags(text: string): string;
|
|
48
|
+
/** 微信 iLink 单条 text 能保留真实换行;多段之间空一行,避免粘成一团。 */
|
|
49
|
+
export declare function formatWechatPlain(text: string): string;
|
|
50
|
+
export declare function looksLikeInternalMonologue(text: string): boolean;
|
|
51
|
+
export declare function usableAssistantText(text: string): string;
|
|
52
|
+
export declare function turnIsBlocked(events: unknown[]): boolean;
|
|
31
53
|
export declare function extractLatestUserText(task: string): string;
|
|
32
54
|
export declare function dshHomeDir(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
33
55
|
export declare function wechatWorkspaceDir(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
@@ -35,7 +57,7 @@ export declare function sessionStorePath(env?: NodeJS.ProcessEnv, home?: string)
|
|
|
35
57
|
export declare function webBaseUrl(env?: NodeJS.ProcessEnv): string;
|
|
36
58
|
export declare function loadSessionStore(path: string): SessionStore;
|
|
37
59
|
export declare function saveSessionStore(path: string, store: SessionStore): void;
|
|
38
|
-
export declare function encodeClientRequest(method: string, payload: Record<string, unknown>, rpcId?:
|
|
60
|
+
export declare function encodeClientRequest(method: string, payload: Record<string, unknown>, rpcId?: string): {
|
|
39
61
|
type: 'client-request';
|
|
40
62
|
rpcId: string;
|
|
41
63
|
method: string;
|
|
@@ -45,6 +67,15 @@ export declare function unwrapRpc(body: unknown): unknown;
|
|
|
45
67
|
export declare function rpcErrorMessage(body: unknown): string | undefined;
|
|
46
68
|
export declare function extractSessionId(body: unknown): string | undefined;
|
|
47
69
|
export declare function extractWorkspaceId(body: unknown): string | undefined;
|
|
70
|
+
export declare function findWorkspaceId(body: unknown, cwd?: string): string | undefined;
|
|
71
|
+
export declare function flattenVisibleText(content: unknown): string;
|
|
72
|
+
export declare function historySettledQuestions(events: unknown[]): boolean;
|
|
73
|
+
export declare function extractPendingQuestions(events: unknown[]): PendingQuestion[];
|
|
74
|
+
export declare function formatQuestionForWechat(questions: PendingQuestion[]): string;
|
|
75
|
+
export declare function parseChoice(userText: string, questions: PendingQuestion[]): {
|
|
76
|
+
id: string;
|
|
77
|
+
label: string;
|
|
78
|
+
} | undefined;
|
|
48
79
|
export declare function historyEvents(body: unknown): unknown[];
|
|
49
80
|
export declare function lastAssistantText(events: unknown[]): string;
|
|
50
81
|
export declare function turnIsIdle(events: unknown[]): boolean;
|
|
@@ -62,7 +93,10 @@ export declare function weixinBridgeEnv(opts: {
|
|
|
62
93
|
launcherPath: string;
|
|
63
94
|
shimScript: string;
|
|
64
95
|
}): NodeJS.ProcessEnv;
|
|
65
|
-
export declare function postRpc(baseUrl: string, method: string, params: Record<string, unknown
|
|
96
|
+
export declare function postRpc(baseUrl: string, method: string, params: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
|
|
97
|
+
export declare function extractArchivedSessionIds(body: unknown): string[];
|
|
98
|
+
/** Stock dsh archives one-way and hides blank sessions. Restore + attach + title. */
|
|
99
|
+
export declare function ensureSessionVisible(rpc: RpcFn, sessionId: string, env?: NodeJS.ProcessEnv, workspaceId?: string, loopback?: boolean): Promise<void>;
|
|
66
100
|
export declare function runHeadlessViaWeb(opts: {
|
|
67
101
|
task: string;
|
|
68
102
|
cwd?: string;
|
package/lib/web-shim.js
CHANGED
|
@@ -13,6 +13,7 @@ import { homedir } from 'node:os';
|
|
|
13
13
|
import { basename, dirname, join } from 'node:path';
|
|
14
14
|
export const WECHAT_WORKSPACE_NAME = '微信';
|
|
15
15
|
export const DEFAULT_WEB_URL = 'http://127.0.0.1:3080';
|
|
16
|
+
export const ASK_USER_QUESTION_TOOL = 'ask_user_question';
|
|
16
17
|
export function parseHeadlessArgv(argv) {
|
|
17
18
|
const args = argv[0]?.endsWith('node') || argv[0]?.includes('node.exe') ? argv.slice(1) : argv;
|
|
18
19
|
const start = args[0]?.endsWith('.js') || args[0]?.endsWith('.mjs') || args[0]?.endsWith('.cjs') ? 1 : 0;
|
|
@@ -27,6 +28,78 @@ export function parseHeadlessArgv(argv) {
|
|
|
27
28
|
export function isFreshBridgeTurn(task) {
|
|
28
29
|
return !task.includes('【桥接上下文】');
|
|
29
30
|
}
|
|
31
|
+
/** WeChat /new is handled by dsh-wechat-bridge. Chinese "重启对话" is a normal message and must be detected here. */
|
|
32
|
+
export function isRestartCommand(text) {
|
|
33
|
+
const t = text.trim();
|
|
34
|
+
if (t.length === 0)
|
|
35
|
+
return false;
|
|
36
|
+
if (/^\/(new|restart|reset|clear)(\s+\S+)?$/i.test(t))
|
|
37
|
+
return true;
|
|
38
|
+
return /^(重启对话|重新开始|新对话|开启新对话|重置对话)[。.!!]?$/.test(t);
|
|
39
|
+
}
|
|
40
|
+
export const RESTART_ACK = '已新开网页会话(左侧「微信」文件夹)。之后直接说话即可。再发「重启对话」会再开一条;旧会话会归档,网页侧栏更干净。';
|
|
41
|
+
export const RESTART_SEED = '新会话已开始。请用一句中文打招呼,不要解释内部步骤。';
|
|
42
|
+
export const STUCK_ACK = '网页端这条会话卡住了(在等批准,或模型把内部推理发出来了)。请看网页左侧「微信」文件夹,或发「重启对话」。';
|
|
43
|
+
export const RPC_TIMEOUT_MS = 15_000;
|
|
44
|
+
export const QUESTION_HINT = '回复数字或选项原文即可。';
|
|
45
|
+
const SKIP_CONTENT_TYPES = new Set(['reasoning', 'thinking', 'tool-call', 'tool-result', 'image']);
|
|
46
|
+
const MONOLOGUE_MARKERS = [
|
|
47
|
+
'sandbox_permissions',
|
|
48
|
+
'danger-full-access',
|
|
49
|
+
'workspace-write',
|
|
50
|
+
'Sandbox mode escalation',
|
|
51
|
+
'escalate speculatively',
|
|
52
|
+
'outside workspace',
|
|
53
|
+
'I need danger-full-access',
|
|
54
|
+
'思考过程',
|
|
55
|
+
'思考内容',
|
|
56
|
+
];
|
|
57
|
+
export function stripThinkTags(text) {
|
|
58
|
+
return text
|
|
59
|
+
.replace(/<think(?:ing|thought)?>[\s\S]*?<\/think(?:ing|thought)?>/gi, '')
|
|
60
|
+
.replace(/<\/?think(?:ing|thought)?>/gi, '');
|
|
61
|
+
}
|
|
62
|
+
/** 微信 iLink 单条 text 能保留真实换行;多段之间空一行,避免粘成一团。 */
|
|
63
|
+
export function formatWechatPlain(text) {
|
|
64
|
+
return text
|
|
65
|
+
.replace(/\r\n?/g, '\n')
|
|
66
|
+
.replace(/[\u2028\u2029]/g, '\n')
|
|
67
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
68
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
69
|
+
.trim();
|
|
70
|
+
}
|
|
71
|
+
export function looksLikeInternalMonologue(text) {
|
|
72
|
+
const trimmed = text.trim();
|
|
73
|
+
if (trimmed.length === 0)
|
|
74
|
+
return false;
|
|
75
|
+
if (MONOLOGUE_MARKERS.some(marker => trimmed.includes(marker)))
|
|
76
|
+
return true;
|
|
77
|
+
if (/^\s*(thought|thinking|reasoning)\b/i.test(trimmed))
|
|
78
|
+
return true;
|
|
79
|
+
if (/<(think|thinking|thought)>/i.test(trimmed) && stripThinkTags(trimmed).trim().length === 0)
|
|
80
|
+
return true;
|
|
81
|
+
return trimmed.length > 400 && /sandbox_permissions|danger-full-access|workspace-write|AbortSignal|justification/i.test(trimmed);
|
|
82
|
+
}
|
|
83
|
+
export function usableAssistantText(text) {
|
|
84
|
+
const trimmed = formatWechatPlain(stripThinkTags(text));
|
|
85
|
+
if (trimmed.length === 0 || looksLikeInternalMonologue(trimmed))
|
|
86
|
+
return '';
|
|
87
|
+
return trimmed;
|
|
88
|
+
}
|
|
89
|
+
export function turnIsBlocked(events) {
|
|
90
|
+
let blocked = false;
|
|
91
|
+
for (const event of events) {
|
|
92
|
+
if (typeof event !== 'object' || event === null)
|
|
93
|
+
continue;
|
|
94
|
+
const type = event.type;
|
|
95
|
+
// Selection / ask_user_question 不是批准卡住:编成纯文本发给微信,不要 STUCK_ACK。
|
|
96
|
+
if (type === 'approval/asked' || type === 'approval/requested')
|
|
97
|
+
blocked = true;
|
|
98
|
+
if (type === 'approval/decided' || type === 'approval/resolved' || type === 'turn/end')
|
|
99
|
+
blocked = false;
|
|
100
|
+
}
|
|
101
|
+
return blocked;
|
|
102
|
+
}
|
|
30
103
|
export function extractLatestUserText(task) {
|
|
31
104
|
if (!task.includes('【桥接上下文】'))
|
|
32
105
|
return task.trim();
|
|
@@ -139,25 +212,339 @@ export function extractWorkspaceId(body) {
|
|
|
139
212
|
if (typeof nested.id === 'string')
|
|
140
213
|
return nested.id;
|
|
141
214
|
}
|
|
215
|
+
// WorkspaceView-like row, or a live Workspace entity serialized with `id`.
|
|
216
|
+
if (typeof rec.id === 'string' && (typeof rec.path === 'string' || typeof rec.title === 'string' || Array.isArray(rec.sessionIds))) {
|
|
217
|
+
return rec.id;
|
|
218
|
+
}
|
|
142
219
|
return undefined;
|
|
143
220
|
}
|
|
144
|
-
function
|
|
221
|
+
function normalizePath(path) {
|
|
222
|
+
return path.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
223
|
+
}
|
|
224
|
+
export function findWorkspaceId(body, cwd) {
|
|
225
|
+
const direct = extractWorkspaceId(body);
|
|
226
|
+
if (direct !== undefined)
|
|
227
|
+
return direct;
|
|
228
|
+
if (typeof body !== 'object' || body === null)
|
|
229
|
+
return undefined;
|
|
230
|
+
const rec = body;
|
|
231
|
+
const items = Array.isArray(rec.items) ? rec.items
|
|
232
|
+
: Array.isArray(body) ? body
|
|
233
|
+
: Array.isArray(rec.workspaces) ? rec.workspaces
|
|
234
|
+
: [];
|
|
235
|
+
const rows = items.filter((item) => typeof item === 'object' && item !== null);
|
|
236
|
+
if (rows.length === 0)
|
|
237
|
+
return undefined;
|
|
238
|
+
const wanted = cwd !== undefined ? normalizePath(cwd) : undefined;
|
|
239
|
+
const basename = wanted?.split('/').pop();
|
|
240
|
+
const match = rows.find(row => {
|
|
241
|
+
const path = typeof row.path === 'string' ? normalizePath(row.path) : undefined;
|
|
242
|
+
const title = typeof row.title === 'string' ? row.title : undefined;
|
|
243
|
+
if (wanted !== undefined && path === wanted)
|
|
244
|
+
return true;
|
|
245
|
+
if (basename !== undefined && title === basename)
|
|
246
|
+
return true;
|
|
247
|
+
if (basename !== undefined && path !== undefined && path.endsWith('/' + basename))
|
|
248
|
+
return true;
|
|
249
|
+
return false;
|
|
250
|
+
}) ?? rows.find(row => row.title === WECHAT_WORKSPACE_NAME);
|
|
251
|
+
return match !== undefined ? extractWorkspaceId(match) : undefined;
|
|
252
|
+
}
|
|
253
|
+
function asRecord(value) {
|
|
254
|
+
return typeof value === 'object' && value !== null ? value : undefined;
|
|
255
|
+
}
|
|
256
|
+
function eventPayload(event) {
|
|
257
|
+
const data = asRecord(event.data);
|
|
258
|
+
return data ?? event;
|
|
259
|
+
}
|
|
260
|
+
export function flattenVisibleText(content) {
|
|
145
261
|
if (typeof content === 'string')
|
|
146
|
-
return content;
|
|
262
|
+
return stripThinkTags(content);
|
|
147
263
|
if (!Array.isArray(content))
|
|
148
264
|
return '';
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (typeof block === 'string')
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
.
|
|
265
|
+
const parts = [];
|
|
266
|
+
for (const block of content) {
|
|
267
|
+
if (typeof block === 'string') {
|
|
268
|
+
const text = stripThinkTags(block).trim();
|
|
269
|
+
if (text !== '')
|
|
270
|
+
parts.push(text);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const rec = asRecord(block);
|
|
274
|
+
if (rec === undefined)
|
|
275
|
+
continue;
|
|
276
|
+
const type = typeof rec.type === 'string' ? rec.type : 'text';
|
|
277
|
+
if (SKIP_CONTENT_TYPES.has(type))
|
|
278
|
+
continue;
|
|
279
|
+
if (type !== 'text' && rec.type !== undefined)
|
|
280
|
+
continue;
|
|
281
|
+
if (typeof rec.text !== 'string')
|
|
282
|
+
continue;
|
|
283
|
+
const text = stripThinkTags(rec.text).trim();
|
|
284
|
+
if (text !== '')
|
|
285
|
+
parts.push(text);
|
|
286
|
+
}
|
|
287
|
+
return parts.join('\n\n');
|
|
288
|
+
}
|
|
289
|
+
function optionLabel(option) {
|
|
290
|
+
if (typeof option === 'string')
|
|
291
|
+
return option.trim();
|
|
292
|
+
const rec = asRecord(option);
|
|
293
|
+
if (rec !== undefined && typeof rec.label === 'string')
|
|
294
|
+
return rec.label.trim();
|
|
295
|
+
return '';
|
|
296
|
+
}
|
|
297
|
+
function questionsFromAskArgs(args, fallbackId) {
|
|
298
|
+
let parsed = args;
|
|
299
|
+
if (typeof args === 'string') {
|
|
300
|
+
try {
|
|
301
|
+
parsed = JSON.parse(args);
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const rec = asRecord(parsed);
|
|
308
|
+
if (rec === undefined)
|
|
309
|
+
return [];
|
|
310
|
+
const rawQuestions = Array.isArray(rec.questions) ? rec.questions : [rec];
|
|
311
|
+
const out = [];
|
|
312
|
+
for (const item of rawQuestions) {
|
|
313
|
+
if (typeof item === 'string') {
|
|
314
|
+
const question = item.trim();
|
|
315
|
+
if (question !== '')
|
|
316
|
+
out.push({ id: fallbackId, question, options: [] });
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const q = asRecord(item);
|
|
320
|
+
if (q === undefined)
|
|
321
|
+
continue;
|
|
322
|
+
const id = typeof q.id === 'string' && q.id.trim() !== '' ? q.id.trim() : fallbackId;
|
|
323
|
+
const question = typeof q.question === 'string' ? q.question.trim()
|
|
324
|
+
: typeof q.prompt === 'string' ? q.prompt.trim()
|
|
325
|
+
: '';
|
|
326
|
+
const header = typeof q.header === 'string' && q.header.trim() !== '' ? q.header.trim() : undefined;
|
|
327
|
+
const options = Array.isArray(q.options) ? q.options.map(optionLabel).filter(label => label !== '') : [];
|
|
328
|
+
if (question === '' && options.length === 0)
|
|
329
|
+
continue;
|
|
330
|
+
const pending = { id, question: question || header || '请选择', options };
|
|
331
|
+
if (header !== undefined)
|
|
332
|
+
pending.header = header;
|
|
333
|
+
out.push(pending);
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
function toolNameOf(rec) {
|
|
338
|
+
const payload = eventPayload(rec);
|
|
339
|
+
const name = payload.name ?? payload.tool ?? payload.toolName ?? rec.name ?? rec.tool ?? rec.toolName;
|
|
340
|
+
return typeof name === 'string' ? name : '';
|
|
341
|
+
}
|
|
342
|
+
function callIdOf(rec) {
|
|
343
|
+
const payload = eventPayload(rec);
|
|
344
|
+
const id = payload.callID ?? payload.callId ?? payload.toolCallId ?? rec.callID ?? rec.callId ?? rec.toolCallId ?? rec.id;
|
|
345
|
+
return typeof id === 'string' ? id : '';
|
|
346
|
+
}
|
|
347
|
+
function isAskUserTool(name) {
|
|
348
|
+
return name === ASK_USER_QUESTION_TOOL || name.endsWith(`__${ASK_USER_QUESTION_TOOL}`);
|
|
349
|
+
}
|
|
350
|
+
function addId(into, value) {
|
|
351
|
+
if (typeof value === 'string' && value.trim() !== '')
|
|
352
|
+
into.add(value.trim());
|
|
353
|
+
}
|
|
354
|
+
function resolvedIdsFrom(rec) {
|
|
355
|
+
const payload = eventPayload(rec);
|
|
356
|
+
const ids = new Set();
|
|
357
|
+
addId(ids, callIdOf(rec));
|
|
358
|
+
addId(ids, payload.questionId);
|
|
359
|
+
addId(ids, payload.requestId);
|
|
360
|
+
addId(ids, rec.questionId);
|
|
361
|
+
addId(ids, rec.requestId);
|
|
362
|
+
const buckets = [payload.answers, rec.answers, payload.questions, rec.questions];
|
|
363
|
+
for (const bucket of buckets) {
|
|
364
|
+
if (!Array.isArray(bucket))
|
|
365
|
+
continue;
|
|
366
|
+
for (const item of bucket) {
|
|
367
|
+
const row = asRecord(item);
|
|
368
|
+
if (row !== undefined)
|
|
369
|
+
addId(ids, row.id);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return [...ids];
|
|
373
|
+
}
|
|
374
|
+
function contentBlocks(rec) {
|
|
375
|
+
const payload = eventPayload(rec);
|
|
376
|
+
const message = asRecord(payload.message) ?? payload;
|
|
377
|
+
const content = message.content ?? payload.content ?? rec.content;
|
|
378
|
+
return Array.isArray(content) ? content : [];
|
|
379
|
+
}
|
|
380
|
+
export function historySettledQuestions(events) {
|
|
381
|
+
for (const event of events) {
|
|
382
|
+
const rec = asRecord(event);
|
|
383
|
+
if (rec === undefined)
|
|
384
|
+
continue;
|
|
385
|
+
const type = typeof rec.type === 'string' ? rec.type : '';
|
|
386
|
+
if (type === 'question/resolved' || type === 'question/answered' || type === 'question/cancelled')
|
|
387
|
+
return true;
|
|
388
|
+
if ((type === 'tool/result' || type === 'tool-result') && isAskUserTool(toolNameOf(rec)))
|
|
389
|
+
return true;
|
|
390
|
+
for (const block of contentBlocks(rec)) {
|
|
391
|
+
const b = asRecord(block);
|
|
392
|
+
if (b === undefined)
|
|
393
|
+
continue;
|
|
394
|
+
if (b.type !== 'tool-result' && b.type !== 'tool/result')
|
|
395
|
+
continue;
|
|
396
|
+
const name = typeof b.toolName === 'string' ? b.toolName : typeof b.name === 'string' ? b.name : '';
|
|
397
|
+
if (isAskUserTool(name))
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
export function extractPendingQuestions(events) {
|
|
404
|
+
const answered = new Set();
|
|
405
|
+
const requestedIds = [];
|
|
406
|
+
const pendingByKey = new Map();
|
|
407
|
+
const pendingOrigins = new Map();
|
|
408
|
+
const keyOf = (item) => `${item.id}:${item.question}:${item.options.join('|')}`;
|
|
409
|
+
const dropAnswered = () => {
|
|
410
|
+
for (const [key, item] of pendingByKey) {
|
|
411
|
+
const origins = pendingOrigins.get(key) ?? new Set([item.id]);
|
|
412
|
+
if ([...origins].some(id => answered.has(id)))
|
|
413
|
+
pendingByKey.delete(key);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
const remember = (items, sourceId) => {
|
|
417
|
+
if (sourceId !== undefined && sourceId !== '')
|
|
418
|
+
requestedIds.push(sourceId);
|
|
419
|
+
for (const item of items) {
|
|
420
|
+
requestedIds.push(item.id);
|
|
421
|
+
const key = keyOf(item);
|
|
422
|
+
const origins = pendingOrigins.get(key) ?? new Set();
|
|
423
|
+
origins.add(item.id);
|
|
424
|
+
if (sourceId !== undefined && sourceId !== '')
|
|
425
|
+
origins.add(sourceId);
|
|
426
|
+
pendingOrigins.set(key, origins);
|
|
427
|
+
if ([...origins].some(id => answered.has(id)))
|
|
428
|
+
continue;
|
|
429
|
+
pendingByKey.set(key, item);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
const markResolved = (rec) => {
|
|
433
|
+
const ids = resolvedIdsFrom(rec);
|
|
434
|
+
if (ids.length === 0) {
|
|
435
|
+
for (const id of requestedIds)
|
|
436
|
+
answered.add(id);
|
|
437
|
+
pendingByKey.clear();
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
for (const id of ids)
|
|
441
|
+
answered.add(id);
|
|
442
|
+
dropAnswered();
|
|
443
|
+
};
|
|
444
|
+
for (const event of events) {
|
|
445
|
+
const rec = asRecord(event);
|
|
446
|
+
if (rec === undefined)
|
|
447
|
+
continue;
|
|
448
|
+
const type = typeof rec.type === 'string' ? rec.type : '';
|
|
449
|
+
const payload = eventPayload(rec);
|
|
450
|
+
if (type === 'question/resolved' || type === 'question/answered' || type === 'question/cancelled') {
|
|
451
|
+
markResolved(rec);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (type === 'question/requested') {
|
|
455
|
+
const sourceId = callIdOf(rec) || 'question';
|
|
456
|
+
remember(questionsFromAskArgs(payload.questions !== undefined ? payload : rec, sourceId), sourceId);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (type === 'tool/result' || type === 'tool-result') {
|
|
460
|
+
if (isAskUserTool(toolNameOf(rec))) {
|
|
461
|
+
const id = callIdOf(rec);
|
|
462
|
+
if (id !== '')
|
|
463
|
+
answered.add(id);
|
|
464
|
+
dropAnswered();
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if ((type === 'tool/call' || type === 'tool-call') && isAskUserTool(toolNameOf(rec))) {
|
|
468
|
+
const id = callIdOf(rec);
|
|
469
|
+
if (id === '' || !answered.has(id)) {
|
|
470
|
+
const args = payload.arguments ?? payload.input ?? payload.params ?? rec.arguments ?? rec.input;
|
|
471
|
+
remember(questionsFromAskArgs(args, id || ASK_USER_QUESTION_TOOL), id);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
for (const block of contentBlocks(rec)) {
|
|
475
|
+
const b = asRecord(block);
|
|
476
|
+
if (b === undefined)
|
|
477
|
+
continue;
|
|
478
|
+
if (b.type === 'tool-result' || b.type === 'tool/result') {
|
|
479
|
+
const name = typeof b.toolName === 'string' ? b.toolName : typeof b.name === 'string' ? b.name : '';
|
|
480
|
+
if (!isAskUserTool(name))
|
|
481
|
+
continue;
|
|
482
|
+
const id = typeof b.toolCallId === 'string' ? b.toolCallId : typeof b.callID === 'string' ? b.callID : '';
|
|
483
|
+
if (id !== '')
|
|
484
|
+
answered.add(id);
|
|
485
|
+
dropAnswered();
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (b.type !== 'tool-call' && b.type !== 'tool/call')
|
|
489
|
+
continue;
|
|
490
|
+
const name = typeof b.toolName === 'string' ? b.toolName : typeof b.name === 'string' ? b.name : '';
|
|
491
|
+
if (!isAskUserTool(name))
|
|
492
|
+
continue;
|
|
493
|
+
const id = typeof b.toolCallId === 'string' ? b.toolCallId : typeof b.callID === 'string' ? b.callID : typeof b.id === 'string' ? b.id : '';
|
|
494
|
+
if (id !== '' && answered.has(id))
|
|
495
|
+
continue;
|
|
496
|
+
remember(questionsFromAskArgs(b.arguments ?? b.input ?? b.params, id || ASK_USER_QUESTION_TOOL), id);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return [...pendingByKey.values()];
|
|
500
|
+
}
|
|
501
|
+
export function formatQuestionForWechat(questions) {
|
|
502
|
+
const parts = [];
|
|
503
|
+
const hint = questions.length > 1
|
|
504
|
+
? '多题时回复如 1-2(第1题第2项),或回复选项原文。'
|
|
505
|
+
: QUESTION_HINT;
|
|
506
|
+
questions.forEach((question, questionIndex) => {
|
|
507
|
+
const title = question.header?.trim() || question.question.trim() || '请选择';
|
|
508
|
+
parts.push(questions.length > 1 ? `${questionIndex + 1}) ${title}` : title);
|
|
509
|
+
if (question.header !== undefined && question.question.trim() !== '' && question.question.trim() !== title) {
|
|
510
|
+
parts.push(question.question.trim());
|
|
511
|
+
}
|
|
512
|
+
question.options.forEach((option, index) => {
|
|
513
|
+
parts.push(`${index + 1}. ${option}`);
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
if (questions.length > 0)
|
|
517
|
+
parts.push(hint);
|
|
518
|
+
return formatWechatPlain(parts.join('\n'));
|
|
519
|
+
}
|
|
520
|
+
export function parseChoice(userText, questions) {
|
|
521
|
+
const raw = userText.trim();
|
|
522
|
+
if (raw === '' || questions.length === 0)
|
|
523
|
+
return undefined;
|
|
524
|
+
const pick = (question, optionIndex) => {
|
|
525
|
+
const label = question?.options[optionIndex];
|
|
526
|
+
if (question === undefined || label === undefined)
|
|
527
|
+
return undefined;
|
|
528
|
+
return { id: question.id, label };
|
|
529
|
+
};
|
|
530
|
+
const dotted = raw.match(/^(\d{1,2})[-..](\d{1,2})$/);
|
|
531
|
+
if (dotted !== null && questions.length > 1) {
|
|
532
|
+
return pick(questions[Number(dotted[1]) - 1], Number(dotted[2]) - 1);
|
|
533
|
+
}
|
|
534
|
+
const numbered = raw.match(/^(\d{1,2})(?:[..、。)]\s*)?$/);
|
|
535
|
+
if (numbered !== null) {
|
|
536
|
+
if (questions.length !== 1)
|
|
537
|
+
return undefined;
|
|
538
|
+
return pick(questions[0], Number(numbered[1]) - 1);
|
|
539
|
+
}
|
|
540
|
+
const exact = [];
|
|
541
|
+
for (const question of questions) {
|
|
542
|
+
for (const option of question.options) {
|
|
543
|
+
if (option === raw || option.trim() === raw)
|
|
544
|
+
exact.push({ id: question.id, label: option });
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return exact.length === 1 ? exact[0] : undefined;
|
|
161
548
|
}
|
|
162
549
|
function unwrapHistoryItem(item) {
|
|
163
550
|
if (typeof item !== 'object' || item === null)
|
|
@@ -192,7 +579,7 @@ export function lastAssistantText(events) {
|
|
|
192
579
|
continue;
|
|
193
580
|
const data = typeof rec.data === 'object' && rec.data !== null ? rec.data : rec;
|
|
194
581
|
const message = typeof data.message === 'object' && data.message !== null ? data.message : data;
|
|
195
|
-
const chunk =
|
|
582
|
+
const chunk = flattenVisibleText(message.content ?? data.content ?? rec.content) || (typeof rec.text === 'string' ? stripThinkTags(rec.text) : '');
|
|
196
583
|
if (chunk.trim() !== '')
|
|
197
584
|
text = chunk;
|
|
198
585
|
}
|
|
@@ -308,7 +695,7 @@ export function weixinBridgeEnv(opts) {
|
|
|
308
695
|
async function sleep(ms) {
|
|
309
696
|
await new Promise(resolve => setTimeout(resolve, ms));
|
|
310
697
|
}
|
|
311
|
-
export async function postRpc(baseUrl, method, params) {
|
|
698
|
+
export async function postRpc(baseUrl, method, params, timeoutMs = RPC_TIMEOUT_MS) {
|
|
312
699
|
const url = `${baseUrl.replace(/\/$/, '')}/api/${method}`;
|
|
313
700
|
const envelope = encodeClientRequest(method, params);
|
|
314
701
|
let response;
|
|
@@ -317,6 +704,7 @@ export async function postRpc(baseUrl, method, params) {
|
|
|
317
704
|
method: 'POST',
|
|
318
705
|
headers: { 'content-type': 'application/json' },
|
|
319
706
|
body: JSON.stringify(envelope),
|
|
707
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
320
708
|
});
|
|
321
709
|
}
|
|
322
710
|
catch (error) {
|
|
@@ -340,10 +728,25 @@ export async function postRpc(baseUrl, method, params) {
|
|
|
340
728
|
}
|
|
341
729
|
async function ensureWorkspace(rpc, cwd) {
|
|
342
730
|
mkdirSync(cwd, { recursive: true });
|
|
343
|
-
|
|
344
|
-
|
|
731
|
+
try {
|
|
732
|
+
const created = await rpc('workspace.create', { path: cwd });
|
|
733
|
+
const id = extractWorkspaceId(created) ?? findWorkspaceId(created, cwd);
|
|
734
|
+
if (id !== undefined)
|
|
735
|
+
return id;
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
// create may 409 / invalid-path; list is the reconnect authority
|
|
739
|
+
}
|
|
740
|
+
try {
|
|
741
|
+
return findWorkspaceId(await rpc('workspace.list', {}), cwd);
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
return undefined;
|
|
745
|
+
}
|
|
345
746
|
}
|
|
346
747
|
async function createSession(rpc, cwd, workspaceId) {
|
|
748
|
+
// Official session.create accepts at most one of workspaceId / cwd.
|
|
749
|
+
// workspaceId is the one that attachSession-s into the 微信 sidebar.
|
|
347
750
|
const payload = workspaceId !== undefined ? { workspaceId } : { cwd };
|
|
348
751
|
const created = await rpc('session.create', payload);
|
|
349
752
|
const id = extractSessionId(created);
|
|
@@ -381,9 +784,111 @@ async function sessionAlive(rpc, sessionId) {
|
|
|
381
784
|
return false;
|
|
382
785
|
}
|
|
383
786
|
}
|
|
384
|
-
|
|
787
|
+
export function extractArchivedSessionIds(body) {
|
|
788
|
+
const unwrapped = unwrapRpc(body);
|
|
789
|
+
if (typeof unwrapped !== 'object' || unwrapped === null)
|
|
790
|
+
return [];
|
|
791
|
+
const rec = unwrapped;
|
|
792
|
+
if (Array.isArray(rec.archivedSessionIds)) {
|
|
793
|
+
return rec.archivedSessionIds.filter((id) => typeof id === 'string');
|
|
794
|
+
}
|
|
795
|
+
if (typeof rec.global === 'object' && rec.global !== null) {
|
|
796
|
+
const nested = rec.global;
|
|
797
|
+
if (Array.isArray(nested.archivedSessionIds)) {
|
|
798
|
+
return nested.archivedSessionIds.filter((id) => typeof id === 'string');
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return [];
|
|
802
|
+
}
|
|
803
|
+
async function tryRpc(rpc, method, params) {
|
|
804
|
+
try {
|
|
805
|
+
await rpc(method, params);
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
catch {
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
/** Stock dsh archives one-way and hides blank sessions. Restore + attach + title. */
|
|
813
|
+
export async function ensureSessionVisible(rpc, sessionId, env = process.env, workspaceId, loopback = true) {
|
|
814
|
+
await tryRpc(rpc, 'workspace.unarchiveSession', { sessionId });
|
|
815
|
+
await tryRpc(rpc, 'session.unarchive', { sessionId });
|
|
816
|
+
if (loopback) {
|
|
817
|
+
const workspacePath = wechatWorkspaceDir(env);
|
|
818
|
+
const url = `${webBaseUrl(env)}/api/dsh-rez-suite/weixin/unarchive`;
|
|
819
|
+
try {
|
|
820
|
+
await fetch(url, {
|
|
821
|
+
method: 'POST',
|
|
822
|
+
headers: { 'content-type': 'application/json' },
|
|
823
|
+
body: JSON.stringify({ sessionId, workspacePath }),
|
|
824
|
+
signal: AbortSignal.timeout(3000),
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
catch {
|
|
828
|
+
// web may be down or an older plugin; prompting still works, sidebar may stay hidden
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (workspaceId !== undefined) {
|
|
832
|
+
await tryRpc(rpc, 'workspace.attachSession', { workspaceId, sessionId });
|
|
833
|
+
await tryRpc(rpc, 'workspace.insertSessionBefore', { workspaceId, sessionId });
|
|
834
|
+
}
|
|
835
|
+
await tryRpc(rpc, 'session.rename', { sessionId, title: WECHAT_WORKSPACE_NAME });
|
|
836
|
+
}
|
|
837
|
+
async function archiveSession(rpc, sessionId) {
|
|
838
|
+
await tryRpc(rpc, 'workspace.archiveSession', { sessionId });
|
|
839
|
+
}
|
|
840
|
+
async function abortSessionTurn(rpc, sessionId) {
|
|
841
|
+
for (const method of ['session.abort', 'session.cancel', 'session.stop', 'session.interrupt']) {
|
|
842
|
+
if (await tryRpc(rpc, method, { sessionId }))
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
async function tryRpcQuick(rpc, method, params, ms = 2000) {
|
|
847
|
+
let timer;
|
|
848
|
+
try {
|
|
849
|
+
await Promise.race([
|
|
850
|
+
rpc(method, params),
|
|
851
|
+
new Promise((_, reject) => {
|
|
852
|
+
timer = setTimeout(() => reject(new Error('timeout')), ms);
|
|
853
|
+
}),
|
|
854
|
+
]);
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
catch {
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
finally {
|
|
861
|
+
if (timer !== undefined)
|
|
862
|
+
clearTimeout(timer);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
async function tryAnswerPendingQuestion(rpc, sessionId, choice) {
|
|
866
|
+
const answer = { answers: [{ id: choice.id, selected: [choice.label] }] };
|
|
867
|
+
const payloads = [
|
|
868
|
+
{ sessionId, answer },
|
|
869
|
+
{ sessionId, answers: answer.answers },
|
|
870
|
+
];
|
|
871
|
+
for (const method of ['question.respond', 'session.respondQuestion']) {
|
|
872
|
+
for (const params of payloads) {
|
|
873
|
+
if (await tryRpcQuick(rpc, method, params))
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return false;
|
|
878
|
+
}
|
|
879
|
+
function composeWechatReply(events, ignorePending) {
|
|
880
|
+
const ignore = new Set((ignorePending ?? []).map(question => question.id));
|
|
881
|
+
const pendingQuestions = extractPendingQuestions(events).filter(question => !ignore.has(question.id));
|
|
882
|
+
const visible = usableAssistantText(lastAssistantText(events));
|
|
883
|
+
if (pendingQuestions.length > 0) {
|
|
884
|
+
const combined = [visible, formatQuestionForWechat(pendingQuestions)].filter(part => part.length > 0).join('\n\n');
|
|
885
|
+
return { text: formatWechatPlain(combined), pendingQuestions };
|
|
886
|
+
}
|
|
887
|
+
return { text: visible, pendingQuestions };
|
|
888
|
+
}
|
|
889
|
+
async function waitForAssistant(rpc, sessionId, beforeText, timeoutMs, ignorePending) {
|
|
385
890
|
const deadline = Date.now() + timeoutMs;
|
|
386
|
-
let last = '';
|
|
891
|
+
let last = { text: '', pendingQuestions: [] };
|
|
387
892
|
let idleOnce = false;
|
|
388
893
|
while (Date.now() < deadline) {
|
|
389
894
|
let events = [];
|
|
@@ -393,21 +898,27 @@ async function waitForAssistant(rpc, sessionId, beforeText, timeoutMs) {
|
|
|
393
898
|
catch {
|
|
394
899
|
events = [];
|
|
395
900
|
}
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
901
|
+
const composed = composeWechatReply(events, ignorePending);
|
|
902
|
+
// 旧题(本轮 prompt 前就在的 Selection)不能立刻发回微信;只发新 turn 里新出现的题。
|
|
903
|
+
if (composed.pendingQuestions.length > 0 && composed.text !== '') {
|
|
904
|
+
return composed;
|
|
905
|
+
}
|
|
906
|
+
if (turnIsBlocked(events))
|
|
907
|
+
return { text: STUCK_ACK, pendingQuestions: [] };
|
|
908
|
+
if (composed.text !== '')
|
|
909
|
+
last = composed;
|
|
399
910
|
const idle = turnIsIdle(events);
|
|
400
911
|
if (idle)
|
|
401
912
|
idleOnce = true;
|
|
402
|
-
if (idle && text !== '' && text !== beforeText)
|
|
403
|
-
return
|
|
404
|
-
if (idleOnce && text !== '' && text !== beforeText)
|
|
405
|
-
return
|
|
913
|
+
if (idle && composed.text !== '' && composed.text !== beforeText)
|
|
914
|
+
return composed;
|
|
915
|
+
if (idleOnce && composed.text !== '' && composed.text !== beforeText)
|
|
916
|
+
return composed;
|
|
406
917
|
await sleep(400);
|
|
407
918
|
}
|
|
408
|
-
if (last !== '' && last !== beforeText)
|
|
919
|
+
if (last.text !== '' && last.text !== beforeText)
|
|
409
920
|
return last;
|
|
410
|
-
|
|
921
|
+
return { text: STUCK_ACK, pendingQuestions: [] };
|
|
411
922
|
}
|
|
412
923
|
export async function runHeadlessViaWeb(opts) {
|
|
413
924
|
const env = opts.env ?? process.env;
|
|
@@ -417,12 +928,16 @@ export async function runHeadlessViaWeb(opts) {
|
|
|
417
928
|
const workspace = wechatWorkspaceDir(env);
|
|
418
929
|
const storeFile = sessionStorePath(env);
|
|
419
930
|
const key = basename(opts.cwd ?? process.cwd()) || 'default';
|
|
931
|
+
const customRpc = opts.rpc !== undefined;
|
|
420
932
|
const rpc = opts.rpc ?? ((method, params) => postRpc(webBaseUrl(env), method, params));
|
|
421
933
|
const timeoutMs = opts.timeoutMs ?? Number(env.DSH_BRIDGE_TIMEOUT_MS ?? 10 * 60 * 1000);
|
|
422
934
|
const workspaceId = await ensureWorkspace(rpc, workspace);
|
|
935
|
+
const restart = isRestartCommand(text);
|
|
936
|
+
const fresh = isFreshBridgeTurn(opts.task) || restart;
|
|
423
937
|
const store = loadSessionStore(storeFile);
|
|
938
|
+
const storedPending = restart ? undefined : store.sessions[key]?.pendingQuestions;
|
|
424
939
|
let sessionId = store.sessions[key]?.sessionId;
|
|
425
|
-
const
|
|
940
|
+
const previousId = sessionId;
|
|
426
941
|
if (sessionId !== undefined && !fresh) {
|
|
427
942
|
const alive = await sessionAlive(rpc, sessionId);
|
|
428
943
|
if (!alive)
|
|
@@ -434,20 +949,65 @@ export async function runHeadlessViaWeb(opts) {
|
|
|
434
949
|
if (sessionId === undefined) {
|
|
435
950
|
sessionId = await createSession(rpc, workspace, workspaceId);
|
|
436
951
|
await maybeSelectModel(rpc, sessionId, env);
|
|
952
|
+
await ensureSessionVisible(rpc, sessionId, env, workspaceId, !customRpc);
|
|
953
|
+
if (previousId !== undefined && previousId !== sessionId) {
|
|
954
|
+
await abortSessionTurn(rpc, previousId);
|
|
955
|
+
await archiveSession(rpc, previousId);
|
|
956
|
+
}
|
|
437
957
|
}
|
|
438
|
-
|
|
439
|
-
|
|
958
|
+
else {
|
|
959
|
+
await ensureSessionVisible(rpc, sessionId, env, workspaceId, !customRpc);
|
|
960
|
+
}
|
|
961
|
+
const promptText = restart ? RESTART_SEED : text;
|
|
440
962
|
let before = '';
|
|
963
|
+
let events = [];
|
|
441
964
|
try {
|
|
442
|
-
|
|
965
|
+
events = historyEvents(await rpc('session.history', { sessionId, maxMessages: 40 }));
|
|
966
|
+
before = usableAssistantText(lastAssistantText(events));
|
|
443
967
|
}
|
|
444
968
|
catch {
|
|
445
969
|
before = '';
|
|
446
970
|
}
|
|
447
|
-
|
|
971
|
+
const pending = extractPendingQuestions(events);
|
|
972
|
+
const questions = pending.length > 0
|
|
973
|
+
? pending
|
|
974
|
+
: (!restart && storedPending !== undefined && storedPending.length > 0 && !historySettledQuestions(events)
|
|
975
|
+
? storedPending
|
|
976
|
+
: []);
|
|
977
|
+
let skipUserPrompt = false;
|
|
978
|
+
if (!restart && questions.length > 0) {
|
|
979
|
+
const choice = parseChoice(text, questions);
|
|
980
|
+
if (choice !== undefined) {
|
|
981
|
+
const answered = await tryAnswerPendingQuestion(rpc, sessionId, choice);
|
|
982
|
+
if (!answered) {
|
|
983
|
+
await abortSessionTurn(rpc, sessionId);
|
|
984
|
+
await rpc('session.prompt', {
|
|
985
|
+
sessionId,
|
|
986
|
+
mode: 'queue',
|
|
987
|
+
content: [{ type: 'text', text: `用户在微信选择了:${choice.label}` }],
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
skipUserPrompt = true;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
if (!skipUserPrompt) {
|
|
994
|
+
if (!turnIsIdle(events) || turnIsBlocked(events)) {
|
|
995
|
+
await abortSessionTurn(rpc, sessionId);
|
|
996
|
+
}
|
|
997
|
+
await rpc('session.prompt', {
|
|
998
|
+
sessionId,
|
|
999
|
+
mode: 'queue',
|
|
1000
|
+
content: [{ type: 'text', text: promptText }],
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
const reply = await waitForAssistant(rpc, sessionId, before, timeoutMs, questions);
|
|
1004
|
+
store.sessions[key] = {
|
|
448
1005
|
sessionId,
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
|
|
1006
|
+
updatedAt: Date.now(),
|
|
1007
|
+
...(reply.pendingQuestions.length > 0 ? { pendingQuestions: reply.pendingQuestions } : {}),
|
|
1008
|
+
};
|
|
1009
|
+
saveSessionStore(storeFile, store);
|
|
1010
|
+
if (restart && (reply.text === STUCK_ACK || reply.text === ''))
|
|
1011
|
+
return RESTART_ACK;
|
|
1012
|
+
return reply.text;
|
|
453
1013
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rezti/dsh-rez-wechat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "ReZ-TI WeChat/WeCom bridges. Personal WeChat is QClaw/ClawBot scan-and-chat via dsh-wechat-bridge; WeCom group send uses wecom-mcp.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -14,15 +14,21 @@
|
|
|
14
14
|
"default": "./lib/index.js"
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
|
+
"dsh": {
|
|
18
|
+
"bundle": {
|
|
19
|
+
"patch": "./cordis.patch.yml"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
17
22
|
"files": [
|
|
18
23
|
"lib/**/*.js",
|
|
19
24
|
"lib/**/*.d.ts",
|
|
25
|
+
"cordis.patch.yml",
|
|
20
26
|
"README.md"
|
|
21
27
|
],
|
|
22
28
|
"license": "Apache-2.0",
|
|
23
29
|
"dependencies": {
|
|
24
30
|
"qrcode": "^1.5.4",
|
|
25
|
-
"@rezti/dsh-rez-sso": "0.1.
|
|
31
|
+
"@rezti/dsh-rez-sso": "0.1.7"
|
|
26
32
|
},
|
|
27
33
|
"devDependencies": {
|
|
28
34
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
@@ -37,7 +43,7 @@
|
|
|
37
43
|
},
|
|
38
44
|
"repository": {
|
|
39
45
|
"type": "git",
|
|
40
|
-
"url": "git+https://github.com/
|
|
46
|
+
"url": "git+https://github.com/ReZ-TI/deepseek-harness.git",
|
|
41
47
|
"directory": "packages/dsh-rez-wechat"
|
|
42
48
|
},
|
|
43
49
|
"scripts": {
|