shennian 0.2.88 → 0.2.89
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/assets/wechat-channel/macos/manifest.json +13 -0
- package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
- package/dist/src/agents/adapter.d.ts +6 -0
- package/dist/src/agents/codex-control.d.ts +35 -0
- package/dist/src/agents/codex-control.js +188 -0
- package/dist/src/agents/codex.d.ts +8 -0
- package/dist/src/agents/codex.js +53 -0
- package/dist/src/channels/base.d.ts +4 -1
- package/dist/src/channels/runtime.d.ts +1 -0
- package/dist/src/channels/runtime.js +32 -1
- package/dist/src/channels/secret-registry.d.ts +1 -0
- package/dist/src/channels/wechat-channel/anchor.d.ts +10 -0
- package/dist/src/channels/wechat-channel/anchor.js +65 -0
- package/dist/src/channels/wechat-channel/client.d.ts +74 -0
- package/dist/src/channels/wechat-channel/client.js +96 -0
- package/dist/src/channels/wechat-channel/cooldown.d.ts +15 -0
- package/dist/src/channels/wechat-channel/cooldown.js +38 -0
- package/dist/src/channels/wechat-channel/fingerprint.d.ts +28 -0
- package/dist/src/channels/wechat-channel/fingerprint.js +71 -0
- package/dist/src/channels/wechat-channel/helper-assets.d.ts +28 -0
- package/dist/src/channels/wechat-channel/helper-assets.js +68 -0
- package/dist/src/channels/wechat-channel/helper-client.d.ts +25 -0
- package/dist/src/channels/wechat-channel/helper-client.js +149 -0
- package/dist/src/channels/wechat-channel/helper-protocol.d.ts +84 -0
- package/dist/src/channels/wechat-channel/helper-protocol.js +115 -0
- package/dist/src/channels/wechat-channel/index.d.ts +16 -0
- package/dist/src/channels/wechat-channel/index.js +19 -0
- package/dist/src/channels/wechat-channel/ledger.d.ts +33 -0
- package/dist/src/channels/wechat-channel/ledger.js +54 -0
- package/dist/src/channels/wechat-channel/media-resolver.d.ts +32 -0
- package/dist/src/channels/wechat-channel/media-resolver.js +181 -0
- package/dist/src/channels/wechat-channel/message-key.d.ts +19 -0
- package/dist/src/channels/wechat-channel/message-key.js +105 -0
- package/dist/src/channels/wechat-channel/observer.d.ts +64 -0
- package/dist/src/channels/wechat-channel/observer.js +118 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +66 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.js +112 -0
- package/dist/src/channels/wechat-channel/preflight.d.ts +37 -0
- package/dist/src/channels/wechat-channel/preflight.js +48 -0
- package/dist/src/channels/wechat-channel/runner.d.ts +34 -0
- package/dist/src/channels/wechat-channel/runner.js +84 -0
- package/dist/src/channels/wechat-channel/runtime.d.ts +45 -0
- package/dist/src/channels/wechat-channel/runtime.js +66 -0
- package/dist/src/channels/wechat-channel/scheduler.d.ts +30 -0
- package/dist/src/channels/wechat-channel/scheduler.js +152 -0
- package/dist/src/channels/wechat-rpa.d.ts +21 -0
- package/dist/src/channels/wechat-rpa.js +12 -6
- package/dist/src/commands/manager.js +2 -0
- package/dist/src/fs/text-decoder.d.ts +10 -0
- package/dist/src/fs/text-decoder.js +110 -0
- package/dist/src/manager/runtime.js +4 -0
- package/dist/src/native-fusion/service.d.ts +10 -0
- package/dist/src/native-fusion/service.js +27 -0
- package/dist/src/session/handlers/chat.js +18 -0
- package/dist/src/session/handlers/fs.js +39 -3
- package/dist/src/session/handlers/session-refresh.js +12 -0
- package/dist/src/session/handlers/tool-detail.d.ts +3 -0
- package/dist/src/session/handlers/tool-detail.js +218 -0
- package/dist/src/session/manager.d.ts +3 -0
- package/dist/src/session/manager.js +58 -0
- package/dist/src/session/types.d.ts +4 -0
- package/package.json +2 -2
- package/dist/scripts/wechat-rpa-download-candidates.mjs +0 -105
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"helperVersion": "0.1.0",
|
|
4
|
+
"protocolVersion": 1,
|
|
5
|
+
"platforms": {
|
|
6
|
+
"darwin": {
|
|
7
|
+
"executable": "shennian-wechat-channel-helper",
|
|
8
|
+
"sha256": "02b8569879cf116d9838474a7f2300f12e3bd252d41369647df2d450936e851d",
|
|
9
|
+
"signed": false,
|
|
10
|
+
"notarized": false
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
Binary file
|
|
@@ -36,6 +36,12 @@ export declare abstract class AgentAdapter extends EventEmitter<AgentAdapterEven
|
|
|
36
36
|
externalChannel?: ExternalChannelSessionStatus | null;
|
|
37
37
|
env?: NodeJS.ProcessEnv;
|
|
38
38
|
}): void;
|
|
39
|
+
getStatus?(): Promise<{
|
|
40
|
+
active: boolean;
|
|
41
|
+
runId?: string | null;
|
|
42
|
+
runPhase?: SessionRunPhase | null;
|
|
43
|
+
canStop?: boolean;
|
|
44
|
+
}>;
|
|
39
45
|
setTitle?(agentSessionId: string, title: string, workDir?: string): Promise<void>;
|
|
40
46
|
abstract start(sessionId: string, workDir: string, agentSessionId?: string | null): Promise<void>;
|
|
41
47
|
abstract send(text: string, modelId?: string, reasoningEffort?: string, attachments?: ChatAttachmentMeta[]): Promise<void>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SessionRunPhase } from '@shennian/wire';
|
|
2
|
+
export type CodexThreadActivity = {
|
|
3
|
+
active: boolean;
|
|
4
|
+
turnId: string | null;
|
|
5
|
+
runPhase: SessionRunPhase | null;
|
|
6
|
+
canStop: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare class CodexAppServerProxyClient {
|
|
9
|
+
private readonly opts;
|
|
10
|
+
private process;
|
|
11
|
+
private rpcSeq;
|
|
12
|
+
private pendingRequests;
|
|
13
|
+
private stderr;
|
|
14
|
+
private initialized;
|
|
15
|
+
constructor(opts?: {
|
|
16
|
+
workDir?: string;
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
});
|
|
19
|
+
start(): Promise<void>;
|
|
20
|
+
initialize(): Promise<void>;
|
|
21
|
+
readThreadActivity(threadId: string): Promise<CodexThreadActivity>;
|
|
22
|
+
interruptThread(threadId: string): Promise<CodexThreadActivity>;
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
private sendRpc;
|
|
25
|
+
private handleMessage;
|
|
26
|
+
private rejectAllPending;
|
|
27
|
+
}
|
|
28
|
+
export declare function probeCodexThreadActivity(params: {
|
|
29
|
+
threadId: string;
|
|
30
|
+
workDir?: string;
|
|
31
|
+
}): Promise<CodexThreadActivity | null>;
|
|
32
|
+
export declare function interruptCodexThread(params: {
|
|
33
|
+
threadId: string;
|
|
34
|
+
workDir?: string;
|
|
35
|
+
}): Promise<CodexThreadActivity | null>;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// @arch docs/architecture/cli/agent-adapters.md
|
|
2
|
+
// @test src/__tests__/codex-control.test.ts
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { resolveBuiltinCommand, spawnAgentCommand } from './command-spec.js';
|
|
5
|
+
import { buildAgentProcessEnv } from '../agent-env.js';
|
|
6
|
+
import { CODEX_APP_SERVER_CLIENT_INFO } from './codex-utils.js';
|
|
7
|
+
function mapThreadStatusToRunPhase(status) {
|
|
8
|
+
const flags = status?.type === 'active' && Array.isArray(status.activeFlags) ? status.activeFlags : [];
|
|
9
|
+
if (flags.includes('waitingOnApproval'))
|
|
10
|
+
return 'waiting_approval';
|
|
11
|
+
if (flags.includes('waitingOnUserInput'))
|
|
12
|
+
return 'waiting_user_input';
|
|
13
|
+
return 'thinking';
|
|
14
|
+
}
|
|
15
|
+
function latestActiveTurnId(turns) {
|
|
16
|
+
if (!Array.isArray(turns))
|
|
17
|
+
return null;
|
|
18
|
+
for (let index = turns.length - 1; index >= 0; index--) {
|
|
19
|
+
const turn = turns[index];
|
|
20
|
+
if (!turn || typeof turn !== 'object')
|
|
21
|
+
continue;
|
|
22
|
+
const record = turn;
|
|
23
|
+
if (record.status !== 'inProgress')
|
|
24
|
+
continue;
|
|
25
|
+
return typeof record.id === 'string' ? record.id : null;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
export class CodexAppServerProxyClient {
|
|
30
|
+
opts;
|
|
31
|
+
process = null;
|
|
32
|
+
rpcSeq = 1;
|
|
33
|
+
pendingRequests = new Map();
|
|
34
|
+
stderr = '';
|
|
35
|
+
initialized = false;
|
|
36
|
+
constructor(opts = {}) {
|
|
37
|
+
this.opts = opts;
|
|
38
|
+
}
|
|
39
|
+
async start() {
|
|
40
|
+
if (this.process)
|
|
41
|
+
return;
|
|
42
|
+
const spec = resolveBuiltinCommand('codex');
|
|
43
|
+
if (!spec)
|
|
44
|
+
throw new Error('Command "codex" not found. Is OpenAI Codex CLI installed?');
|
|
45
|
+
const proc = spawnAgentCommand(spec, ['app-server', 'proxy'], {
|
|
46
|
+
cwd: this.opts.workDir || undefined,
|
|
47
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
48
|
+
env: buildAgentProcessEnv({ NO_COLOR: '1' }),
|
|
49
|
+
});
|
|
50
|
+
this.process = proc;
|
|
51
|
+
this.stderr = '';
|
|
52
|
+
const rl = createInterface({ input: proc.stdout });
|
|
53
|
+
rl.on('line', (line) => {
|
|
54
|
+
if (!line.trim())
|
|
55
|
+
return;
|
|
56
|
+
let msg;
|
|
57
|
+
try {
|
|
58
|
+
msg = JSON.parse(line);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.handleMessage(msg);
|
|
64
|
+
});
|
|
65
|
+
proc.stderr?.on('data', (chunk) => {
|
|
66
|
+
this.stderr += chunk.toString();
|
|
67
|
+
});
|
|
68
|
+
proc.on('close', (code) => {
|
|
69
|
+
if (this.process !== proc)
|
|
70
|
+
return;
|
|
71
|
+
this.process = null;
|
|
72
|
+
this.rejectAllPending(new Error(this.stderr.trim() || `codex app-server proxy exited with code ${code}`));
|
|
73
|
+
});
|
|
74
|
+
proc.on('error', (error) => {
|
|
75
|
+
if (this.process !== proc)
|
|
76
|
+
return;
|
|
77
|
+
this.process = null;
|
|
78
|
+
this.rejectAllPending(error);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async initialize() {
|
|
82
|
+
if (this.initialized)
|
|
83
|
+
return;
|
|
84
|
+
await this.start();
|
|
85
|
+
await this.sendRpc('initialize', {
|
|
86
|
+
clientInfo: CODEX_APP_SERVER_CLIENT_INFO,
|
|
87
|
+
capabilities: { experimentalApi: true },
|
|
88
|
+
});
|
|
89
|
+
this.initialized = true;
|
|
90
|
+
}
|
|
91
|
+
async readThreadActivity(threadId) {
|
|
92
|
+
await this.initialize();
|
|
93
|
+
const response = await this.sendRpc('thread/read', { threadId, includeTurns: true });
|
|
94
|
+
const status = response.thread?.status ?? null;
|
|
95
|
+
const active = status?.type === 'active';
|
|
96
|
+
const turnId = latestActiveTurnId(response.thread?.turns);
|
|
97
|
+
return {
|
|
98
|
+
active,
|
|
99
|
+
turnId,
|
|
100
|
+
runPhase: active ? mapThreadStatusToRunPhase(status) : null,
|
|
101
|
+
canStop: Boolean(active && turnId),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async interruptThread(threadId) {
|
|
105
|
+
const activity = await this.readThreadActivity(threadId);
|
|
106
|
+
if (!activity.turnId)
|
|
107
|
+
return activity;
|
|
108
|
+
await this.sendRpc('turn/interrupt', { threadId, turnId: activity.turnId }, 5_000);
|
|
109
|
+
return { active: false, turnId: activity.turnId, runPhase: null, canStop: false };
|
|
110
|
+
}
|
|
111
|
+
async close() {
|
|
112
|
+
const proc = this.process;
|
|
113
|
+
this.process = null;
|
|
114
|
+
if (!proc)
|
|
115
|
+
return;
|
|
116
|
+
proc.kill('SIGTERM');
|
|
117
|
+
await new Promise((resolve) => {
|
|
118
|
+
proc.on('close', resolve);
|
|
119
|
+
setTimeout(() => {
|
|
120
|
+
proc.kill('SIGKILL');
|
|
121
|
+
resolve();
|
|
122
|
+
}, 1000).unref?.();
|
|
123
|
+
});
|
|
124
|
+
this.rejectAllPending(new Error('codex app-server proxy stopped'));
|
|
125
|
+
}
|
|
126
|
+
sendRpc(method, params, timeoutMs = this.opts.timeoutMs ?? 8_000) {
|
|
127
|
+
const proc = this.process;
|
|
128
|
+
if (!proc?.stdin)
|
|
129
|
+
return Promise.reject(new Error('codex app-server proxy is not running'));
|
|
130
|
+
const id = this.rpcSeq++;
|
|
131
|
+
const payload = JSON.stringify({ id, method, params });
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
const timer = setTimeout(() => {
|
|
134
|
+
this.pendingRequests.delete(id);
|
|
135
|
+
reject(new Error(`codex app-server proxy request timed out: ${method}`));
|
|
136
|
+
}, timeoutMs);
|
|
137
|
+
this.pendingRequests.set(id, { resolve, reject, timer });
|
|
138
|
+
proc.stdin.write(`${payload}\n`, (error) => {
|
|
139
|
+
if (!error)
|
|
140
|
+
return;
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
this.pendingRequests.delete(id);
|
|
143
|
+
reject(error);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
handleMessage(msg) {
|
|
148
|
+
if (msg.id == null)
|
|
149
|
+
return;
|
|
150
|
+
const pending = this.pendingRequests.get(msg.id);
|
|
151
|
+
if (!pending)
|
|
152
|
+
return;
|
|
153
|
+
clearTimeout(pending.timer);
|
|
154
|
+
this.pendingRequests.delete(msg.id);
|
|
155
|
+
if (msg.error)
|
|
156
|
+
pending.reject(new Error(msg.error.message || `codex app-server proxy error ${msg.error.code ?? ''}`.trim()));
|
|
157
|
+
else
|
|
158
|
+
pending.resolve(msg.result);
|
|
159
|
+
}
|
|
160
|
+
rejectAllPending(error) {
|
|
161
|
+
for (const [id, pending] of this.pendingRequests.entries()) {
|
|
162
|
+
clearTimeout(pending.timer);
|
|
163
|
+
pending.reject(error);
|
|
164
|
+
this.pendingRequests.delete(id);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export async function probeCodexThreadActivity(params) {
|
|
169
|
+
const client = new CodexAppServerProxyClient({ workDir: params.workDir, timeoutMs: 5_000 });
|
|
170
|
+
try {
|
|
171
|
+
return await client.readThreadActivity(params.threadId);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
await client.close().catch(() => { });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
export async function interruptCodexThread(params) {
|
|
181
|
+
const client = new CodexAppServerProxyClient({ workDir: params.workDir, timeoutMs: 8_000 });
|
|
182
|
+
try {
|
|
183
|
+
return await client.interruptThread(params.threadId);
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
await client.close().catch(() => { });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AgentAdapter } from './adapter.js';
|
|
2
2
|
import type { ChatAttachmentMeta, ExternalChannelSessionStatus } from '@shennian/wire';
|
|
3
|
+
import type { SessionRunPhase } from '@shennian/wire';
|
|
3
4
|
export { isCodexUnsupportedEffortError, isMissingCodexRolloutError, normalizeCodexModelId, normalizeCodexReasoningEffort, } from './codex-utils.js';
|
|
4
5
|
export declare class CodexAdapter extends AgentAdapter {
|
|
5
6
|
private readonly options;
|
|
@@ -40,6 +41,12 @@ export declare class CodexAdapter extends AgentAdapter {
|
|
|
40
41
|
resume(agentSessionId: string): Promise<void>;
|
|
41
42
|
setTitle(agentSessionId: string, title: string, workDir?: string): Promise<void>;
|
|
42
43
|
stop(): Promise<void>;
|
|
44
|
+
getStatus(): Promise<{
|
|
45
|
+
active: boolean;
|
|
46
|
+
runId?: string | null;
|
|
47
|
+
runPhase?: SessionRunPhase | null;
|
|
48
|
+
canStop?: boolean;
|
|
49
|
+
}>;
|
|
43
50
|
private spawnCodex;
|
|
44
51
|
private spawnAppServer;
|
|
45
52
|
private ensureAppServer;
|
|
@@ -49,6 +56,7 @@ export declare class CodexAdapter extends AgentAdapter {
|
|
|
49
56
|
private interruptActiveTurn;
|
|
50
57
|
private sendRpc;
|
|
51
58
|
private handleAppServerMessage;
|
|
59
|
+
private mapThreadStatusToRunPhase;
|
|
52
60
|
private handleAppServerNonJsonStdout;
|
|
53
61
|
private failAppServerStartup;
|
|
54
62
|
private handleAppServerCompletedItem;
|
package/dist/src/agents/codex.js
CHANGED
|
@@ -117,6 +117,30 @@ export class CodexAdapter extends AgentAdapter {
|
|
|
117
117
|
await this.interruptActiveTurn().catch(() => { });
|
|
118
118
|
await this.killProcess();
|
|
119
119
|
}
|
|
120
|
+
async getStatus() {
|
|
121
|
+
const threadId = this.agentSessionId;
|
|
122
|
+
const activeTurnId = this.activeTurnId;
|
|
123
|
+
if (!threadId || !this.process) {
|
|
124
|
+
return { active: false, runId: activeTurnId, runPhase: null, canStop: false };
|
|
125
|
+
}
|
|
126
|
+
let status = null;
|
|
127
|
+
try {
|
|
128
|
+
const response = await this.sendRpc('thread/read', { threadId, includeTurns: false }, 5_000);
|
|
129
|
+
status = response.thread?.status ?? null;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// If the local control channel is still alive but the status read fails,
|
|
133
|
+
// fall back to activeTurnId. This keeps the stop button available for
|
|
134
|
+
// the running turn while avoiding false positives for idle sessions.
|
|
135
|
+
}
|
|
136
|
+
const active = status?.type === 'active' || Boolean(activeTurnId);
|
|
137
|
+
return {
|
|
138
|
+
active,
|
|
139
|
+
runId: activeTurnId,
|
|
140
|
+
runPhase: active ? this.mapThreadStatusToRunPhase(status) : null,
|
|
141
|
+
canStop: Boolean(active && activeTurnId && this.process),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
120
144
|
spawnCodex(args) {
|
|
121
145
|
const spec = resolveBuiltinCommand('codex');
|
|
122
146
|
if (!spec) {
|
|
@@ -351,6 +375,27 @@ export class CodexAdapter extends AgentAdapter {
|
|
|
351
375
|
return;
|
|
352
376
|
const params = msg.params ?? {};
|
|
353
377
|
switch (msg.method) {
|
|
378
|
+
case 'turn/started': {
|
|
379
|
+
const turn = typeof params.turn === 'object' && params.turn !== null
|
|
380
|
+
? params.turn
|
|
381
|
+
: null;
|
|
382
|
+
const turnId = typeof turn?.id === 'string' ? turn.id : null;
|
|
383
|
+
if (turnId)
|
|
384
|
+
this.activeTurnId = turnId;
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
case 'thread/status/changed': {
|
|
388
|
+
const threadId = typeof params.threadId === 'string' ? params.threadId : '';
|
|
389
|
+
if (threadId && this.agentSessionId && threadId !== this.agentSessionId)
|
|
390
|
+
break;
|
|
391
|
+
const status = typeof params.status === 'object' && params.status !== null
|
|
392
|
+
? params.status
|
|
393
|
+
: null;
|
|
394
|
+
if (status?.type === 'active') {
|
|
395
|
+
this.emitEvent({ state: 'heartbeat', runPhase: this.mapThreadStatusToRunPhase(status) });
|
|
396
|
+
}
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
354
399
|
case 'item/agentMessage/delta': {
|
|
355
400
|
const itemId = typeof params.itemId === 'string' ? params.itemId : '';
|
|
356
401
|
if (itemId)
|
|
@@ -387,6 +432,14 @@ export class CodexAdapter extends AgentAdapter {
|
|
|
387
432
|
}
|
|
388
433
|
}
|
|
389
434
|
}
|
|
435
|
+
mapThreadStatusToRunPhase(status) {
|
|
436
|
+
const flags = status?.type === 'active' && Array.isArray(status.activeFlags) ? status.activeFlags : [];
|
|
437
|
+
if (flags.includes('waitingOnApproval'))
|
|
438
|
+
return 'waiting_approval';
|
|
439
|
+
if (flags.includes('waitingOnUserInput'))
|
|
440
|
+
return 'waiting_user_input';
|
|
441
|
+
return 'thinking';
|
|
442
|
+
}
|
|
390
443
|
handleAppServerNonJsonStdout(line) {
|
|
391
444
|
const normalized = normalizeTerminalText(line);
|
|
392
445
|
if (!looksLikeCodexInteractiveAuthPrompt(normalized))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExternalChannelSessionStatus } from '@shennian/wire';
|
|
2
|
-
export type ExternalChannelRuntimeStatus = Pick<ExternalChannelSessionStatus, 'wechatRpaRuntimeState' | 'wechatRpaLastRunAt' | 'wechatRpaLastMessageAt' | 'wechatRpaLastInterruptedAt' | 'wechatRpaPendingReplyCount' | 'wechatRpaLastError' | 'wechatRpaLastRunId' | 'wechatRpaLastTracePath' | 'wechatRpaLastTraceSummary' | 'wechatRpaRecentTaskSummaries'>;
|
|
2
|
+
export type ExternalChannelRuntimeStatus = Pick<ExternalChannelSessionStatus, 'wechatRpaRuntimeState' | 'wechatRpaLastRunAt' | 'wechatRpaLastMessageAt' | 'wechatRpaLastInterruptedAt' | 'wechatRpaPendingReplyCount' | 'wechatRpaLastError' | 'wechatRpaLastRunId' | 'wechatRpaLastTracePath' | 'wechatRpaLastTraceSummary' | 'wechatRpaRecentTaskSummaries' | 'wechatRpaPreflightChecks' | 'wechatRpaServerDecisionAvailable' | 'wechatRpaPrivacyConsentAccepted'>;
|
|
3
3
|
export type ExternalChannelType = 'wecom' | 'websocket' | 'wechat-rpa';
|
|
4
4
|
export type ExternalChannelConfig = {
|
|
5
5
|
id: string;
|
|
@@ -51,6 +51,9 @@ export type ExternalChannelView = {
|
|
|
51
51
|
wechatRpaLastTracePath?: string | null;
|
|
52
52
|
wechatRpaLastTraceSummary?: string | null;
|
|
53
53
|
wechatRpaRecentTaskSummaries?: ExternalChannelSessionStatus['wechatRpaRecentTaskSummaries'];
|
|
54
|
+
wechatRpaPreflightChecks?: ExternalChannelSessionStatus['wechatRpaPreflightChecks'];
|
|
55
|
+
wechatRpaServerDecisionAvailable?: boolean | null;
|
|
56
|
+
wechatRpaPrivacyConsentAccepted?: boolean | null;
|
|
54
57
|
};
|
|
55
58
|
export type ExternalMessageEvent = {
|
|
56
59
|
type: 'external.message';
|
|
@@ -350,6 +350,8 @@ export class ChannelRuntime {
|
|
|
350
350
|
};
|
|
351
351
|
const priorSecret = this.secrets.get(nextConfig.secretRef);
|
|
352
352
|
const source = input.source || (priorSecret?.source === 'macos-probe' || priorSecret?.source === 'fixture-jsonl' || priorSecret?.source === 'macos-flow' || priorSecret?.source === 'windows-visual-flow' || priorSecret?.source === 'wechat-rpa-lab' ? priorSecret.source : defaultWeChatRpaSource());
|
|
353
|
+
if (input.enabled && source === 'windows-visual-flow')
|
|
354
|
+
throw new Error('个人微信通道当前仅支持 macOS');
|
|
353
355
|
const configs = allConfigs
|
|
354
356
|
.filter((channel) => channel.id !== nextConfig.id)
|
|
355
357
|
.map((channel) => (channel.sessionId ?? channel.managerSessionId) === boundSessionId && channel.type === 'wechat-rpa'
|
|
@@ -369,6 +371,7 @@ export class ChannelRuntime {
|
|
|
369
371
|
downloadAttachments: input.downloadAttachments ?? (priorSecret?.downloadAttachments === undefined ? true : Boolean(priorSecret.downloadAttachments)),
|
|
370
372
|
downloadAttachmentsDir: input.downloadAttachmentsDir?.trim() || stringOrUndefined(priorSecret?.downloadAttachmentsDir),
|
|
371
373
|
selfNickname: input.selfNickname?.trim() || stringOrUndefined(priorSecret?.selfNickname),
|
|
374
|
+
privacyConsentAccepted: input.privacyConsentAccepted ?? Boolean(priorSecret?.privacyConsentAccepted),
|
|
372
375
|
flowScriptPath: input.flowScriptPath?.trim() || stringOrUndefined(priorSecret?.flowScriptPath),
|
|
373
376
|
canReply: input.canReply ?? priorSecret?.canReply ?? false,
|
|
374
377
|
systemPrompt: input.systemPrompt ?? (typeof priorSecret?.systemPrompt === 'string' ? priorSecret.systemPrompt : ''),
|
|
@@ -410,6 +413,9 @@ function wechatRpaViewFields(secret) {
|
|
|
410
413
|
downloadAttachments: secret.downloadAttachments === undefined ? true : Boolean(secret.downloadAttachments),
|
|
411
414
|
downloadAttachmentsDir: typeof secret.downloadAttachmentsDir === 'string' ? secret.downloadAttachmentsDir : '',
|
|
412
415
|
selfNickname: typeof secret.selfNickname === 'string' ? secret.selfNickname : '',
|
|
416
|
+
wechatRpaPrivacyConsentAccepted: Boolean(secret.privacyConsentAccepted),
|
|
417
|
+
wechatRpaServerDecisionAvailable: true,
|
|
418
|
+
wechatRpaPreflightChecks: buildWeChatRpaPreflightChecks(secret),
|
|
413
419
|
};
|
|
414
420
|
}
|
|
415
421
|
function wechatRpaStatusFields(secret) {
|
|
@@ -426,6 +432,9 @@ function wechatRpaStatusFields(secret) {
|
|
|
426
432
|
downloadAttachments: secret.downloadAttachments === undefined ? true : Boolean(secret.downloadAttachments),
|
|
427
433
|
downloadAttachmentsDir: typeof secret.downloadAttachmentsDir === 'string' ? secret.downloadAttachmentsDir : null,
|
|
428
434
|
selfNickname: typeof secret.selfNickname === 'string' ? secret.selfNickname : null,
|
|
435
|
+
wechatRpaPrivacyConsentAccepted: Boolean(secret.privacyConsentAccepted),
|
|
436
|
+
wechatRpaServerDecisionAvailable: true,
|
|
437
|
+
wechatRpaPreflightChecks: buildWeChatRpaPreflightChecks(secret),
|
|
429
438
|
};
|
|
430
439
|
}
|
|
431
440
|
function normalizeWeChatRpaGroups(groups) {
|
|
@@ -441,7 +450,29 @@ function normalizeWeChatRpaGroups(groups) {
|
|
|
441
450
|
return result;
|
|
442
451
|
}
|
|
443
452
|
function defaultWeChatRpaSource() {
|
|
444
|
-
return '
|
|
453
|
+
return 'macos-flow';
|
|
454
|
+
}
|
|
455
|
+
function buildWeChatRpaPreflightChecks(secret) {
|
|
456
|
+
const checks = [];
|
|
457
|
+
checks.push({
|
|
458
|
+
code: 'mac_only',
|
|
459
|
+
ok: secret.source !== 'windows-visual-flow',
|
|
460
|
+
severity: 'blocking',
|
|
461
|
+
message: secret.source === 'windows-visual-flow' ? '个人微信通道当前仅支持 macOS。' : '当前配置使用 macOS 微信通道。',
|
|
462
|
+
});
|
|
463
|
+
checks.push({
|
|
464
|
+
code: 'privacy_consent_required',
|
|
465
|
+
ok: Boolean(secret.privacyConsentAccepted),
|
|
466
|
+
severity: 'blocking',
|
|
467
|
+
message: Boolean(secret.privacyConsentAccepted) ? '已确认微信通道数据与隐私授权。' : '启用前需要确认微信通道数据与隐私授权。',
|
|
468
|
+
});
|
|
469
|
+
checks.push({
|
|
470
|
+
code: 'server_decision_unavailable',
|
|
471
|
+
ok: true,
|
|
472
|
+
severity: 'blocking',
|
|
473
|
+
message: '服务端判断能力可用。',
|
|
474
|
+
});
|
|
475
|
+
return checks;
|
|
445
476
|
}
|
|
446
477
|
export function planExternalReplySends(channelType, input) {
|
|
447
478
|
const parts = splitExternalReplyText(input.text);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { WeChatChannelObservedMessage } from './client.js';
|
|
2
|
+
export declare function normalizeWeChatAnchorText(value: unknown): string;
|
|
3
|
+
export declare function weChatAnchorText(message: WeChatChannelObservedMessage): string;
|
|
4
|
+
export declare function weChatTextSimilarity(left: unknown, right: unknown): number;
|
|
5
|
+
export declare function isLikelySameWeChatMessage(previous: WeChatChannelObservedMessage, current: WeChatChannelObservedMessage, threshold?: number): boolean;
|
|
6
|
+
export declare function filterNewWeChatMessagesByAnchor(input: {
|
|
7
|
+
previous: WeChatChannelObservedMessage[];
|
|
8
|
+
current: WeChatChannelObservedMessage[];
|
|
9
|
+
threshold?: number;
|
|
10
|
+
}): WeChatChannelObservedMessage[];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// @arch docs/features/wechat-rpa-productization-plan.md
|
|
2
|
+
// @test src/__tests__/wechat-channel-anchor.test.ts
|
|
3
|
+
export function normalizeWeChatAnchorText(value) {
|
|
4
|
+
if (typeof value !== 'string')
|
|
5
|
+
return '';
|
|
6
|
+
return value
|
|
7
|
+
.normalize('NFKC')
|
|
8
|
+
.replace(/\s+/g, ' ')
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
export function weChatAnchorText(message) {
|
|
13
|
+
return normalizeWeChatAnchorText(message.anchorText || message.normalizedText || message.textExcerpt || '');
|
|
14
|
+
}
|
|
15
|
+
export function weChatTextSimilarity(left, right) {
|
|
16
|
+
const a = normalizeWeChatAnchorText(left);
|
|
17
|
+
const b = normalizeWeChatAnchorText(right);
|
|
18
|
+
if (!a || !b)
|
|
19
|
+
return 0;
|
|
20
|
+
if (a === b)
|
|
21
|
+
return 1;
|
|
22
|
+
const distance = levenshtein(a, b);
|
|
23
|
+
return 1 - distance / Math.max(a.length, b.length);
|
|
24
|
+
}
|
|
25
|
+
export function isLikelySameWeChatMessage(previous, current, threshold = 0.86) {
|
|
26
|
+
if (previous.stableMessageKey && previous.stableMessageKey === current.stableMessageKey)
|
|
27
|
+
return true;
|
|
28
|
+
if (previous.senderRole !== current.senderRole)
|
|
29
|
+
return false;
|
|
30
|
+
if (previous.kind !== current.kind)
|
|
31
|
+
return false;
|
|
32
|
+
const previousAnchor = weChatAnchorText(previous);
|
|
33
|
+
const currentAnchor = weChatAnchorText(current);
|
|
34
|
+
if (!previousAnchor || !currentAnchor)
|
|
35
|
+
return false;
|
|
36
|
+
return weChatTextSimilarity(previousAnchor, currentAnchor) >= threshold;
|
|
37
|
+
}
|
|
38
|
+
export function filterNewWeChatMessagesByAnchor(input) {
|
|
39
|
+
const consumed = new Set();
|
|
40
|
+
const result = [];
|
|
41
|
+
for (const current of input.current) {
|
|
42
|
+
const index = input.previous.findIndex((previous, previousIndex) => {
|
|
43
|
+
return !consumed.has(previousIndex) && isLikelySameWeChatMessage(previous, current, input.threshold);
|
|
44
|
+
});
|
|
45
|
+
if (index >= 0) {
|
|
46
|
+
consumed.add(index);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
result.push(current);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
function levenshtein(a, b) {
|
|
54
|
+
const prev = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
55
|
+
const curr = Array.from({ length: b.length + 1 }, () => 0);
|
|
56
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
57
|
+
curr[0] = i;
|
|
58
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
59
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
60
|
+
}
|
|
61
|
+
for (let j = 0; j <= b.length; j += 1)
|
|
62
|
+
prev[j] = curr[j];
|
|
63
|
+
}
|
|
64
|
+
return prev[b.length];
|
|
65
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { WeChatChannelRuntime, WeChatChannelBindingConfig } from './runtime.js';
|
|
2
|
+
export type WeChatChannelApiClientOptions = {
|
|
3
|
+
serverUrl?: string;
|
|
4
|
+
machineToken?: string;
|
|
5
|
+
fetchImpl?: typeof fetch;
|
|
6
|
+
};
|
|
7
|
+
export type WeChatChannelObservedMessage = {
|
|
8
|
+
stableMessageKey: string;
|
|
9
|
+
senderRole: 'self' | 'contact' | 'system' | 'unknown';
|
|
10
|
+
senderName?: string | null;
|
|
11
|
+
kind: string;
|
|
12
|
+
normalizedText?: string | null;
|
|
13
|
+
anchorText?: string | null;
|
|
14
|
+
anchorMetadata?: unknown;
|
|
15
|
+
neighborContext?: unknown;
|
|
16
|
+
textExcerpt?: string | null;
|
|
17
|
+
bbox?: unknown;
|
|
18
|
+
mediaMetadata?: unknown;
|
|
19
|
+
isBaseline?: boolean;
|
|
20
|
+
deliveryStatus?: string;
|
|
21
|
+
observedAt?: string;
|
|
22
|
+
visualBlocks?: Array<{
|
|
23
|
+
blockId: string;
|
|
24
|
+
blockKind: string;
|
|
25
|
+
bbox?: unknown;
|
|
26
|
+
model: string;
|
|
27
|
+
dims: number;
|
|
28
|
+
vectorBase64: string;
|
|
29
|
+
}>;
|
|
30
|
+
};
|
|
31
|
+
export type WeChatChannelApiClient = ReturnType<typeof createWeChatChannelApiClient>;
|
|
32
|
+
export type WeChatChannelObserveInput = {
|
|
33
|
+
screenshots: Array<{
|
|
34
|
+
captureIndex?: number;
|
|
35
|
+
mimeType: string;
|
|
36
|
+
dataBase64: string;
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
}>;
|
|
40
|
+
edgeOcrBlocks?: unknown[];
|
|
41
|
+
visibleConversationFingerprints?: unknown[];
|
|
42
|
+
localLedgerTailAnchors?: unknown[];
|
|
43
|
+
};
|
|
44
|
+
export type WeChatChannelObserveResponse = {
|
|
45
|
+
ok: true;
|
|
46
|
+
observedMessages?: WeChatChannelObservedMessage[];
|
|
47
|
+
diff?: unknown;
|
|
48
|
+
reasonCode?: string;
|
|
49
|
+
};
|
|
50
|
+
export type WeChatChannelOutboundStatusInput = {
|
|
51
|
+
replyId: string;
|
|
52
|
+
idempotencyKey: string;
|
|
53
|
+
status: string;
|
|
54
|
+
replyBaseRevision?: number;
|
|
55
|
+
sentAt?: string;
|
|
56
|
+
confirmedAt?: string;
|
|
57
|
+
failureCode?: string;
|
|
58
|
+
lastErrorSummary?: string;
|
|
59
|
+
};
|
|
60
|
+
export declare function createWeChatChannelApiClient(options?: WeChatChannelApiClientOptions): {
|
|
61
|
+
getRuntimePolicy: () => Promise<unknown>;
|
|
62
|
+
upsertRuntime: (runtime: WeChatChannelRuntime, binding?: WeChatChannelBindingConfig) => Promise<unknown>;
|
|
63
|
+
observe: (runtime: WeChatChannelRuntime, binding: WeChatChannelBindingConfig, input: WeChatChannelObserveInput) => Promise<WeChatChannelObserveResponse>;
|
|
64
|
+
ingest: (runtime: WeChatChannelRuntime, binding: WeChatChannelBindingConfig, input: {
|
|
65
|
+
idempotencyKey: string;
|
|
66
|
+
messages: WeChatChannelObservedMessage[];
|
|
67
|
+
}) => Promise<unknown>;
|
|
68
|
+
reportOutboundStatus: (runtime: WeChatChannelRuntime, binding: WeChatChannelBindingConfig, input: WeChatChannelOutboundStatusInput) => Promise<unknown>;
|
|
69
|
+
reportRunStatus: (runtime: WeChatChannelRuntime, binding: WeChatChannelBindingConfig, input: {
|
|
70
|
+
status: string;
|
|
71
|
+
reasonCode?: string;
|
|
72
|
+
traceId?: string;
|
|
73
|
+
}) => Promise<unknown>;
|
|
74
|
+
};
|