shennian 0.2.87 → 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-utils.d.ts +5 -0
- package/dist/src/agents/codex-utils.js +5 -0
- package/dist/src/agents/codex.d.ts +8 -0
- package/dist/src/agents/codex.js +55 -2
- package/dist/src/agents/model-registry/discovery.js +2 -1
- package/dist/src/channels/base.d.ts +4 -13
- package/dist/src/channels/runtime.d.ts +1 -3
- package/dist/src/channels/runtime.js +32 -5
- package/dist/src/channels/secret-registry.d.ts +1 -4
- 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/macos-flow.d.ts +0 -28
- package/dist/src/channels/wechat-rpa/macos-flow.js +1 -134
- package/dist/src/channels/wechat-rpa.d.ts +21 -0
- package/dist/src/channels/wechat-rpa.js +39 -61
- package/dist/src/commands/manager.d.ts +1 -1
- package/dist/src/commands/manager.js +5 -10
- 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 -6
- 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 -2
- 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
- package/dist/scripts/wechat-rpa-win-visual.mjs +0 -1735
- package/dist/scripts/wechat-rpa-win.mjs +0 -352
- package/dist/src/channels/wechat-rpa/windows-visual-flow.d.ts +0 -40
- package/dist/src/channels/wechat-rpa/windows-visual-flow.js +0 -189
|
@@ -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,4 +1,9 @@
|
|
|
1
1
|
export declare function makeThreadTitle(text: string): string;
|
|
2
|
+
export declare const CODEX_APP_SERVER_CLIENT_INFO: {
|
|
3
|
+
readonly name: "codex-tui";
|
|
4
|
+
readonly title: "Codex TUI";
|
|
5
|
+
readonly version: "0.0.0";
|
|
6
|
+
};
|
|
2
7
|
export declare function isCodexCollabAgentToolName(name: string): boolean;
|
|
3
8
|
export declare function isCodexCollabAgentItem(item: Record<string, unknown>): boolean;
|
|
4
9
|
export declare function isCodexLegacyCollabAgentItem(item: {
|
|
@@ -5,6 +5,11 @@ export function makeThreadTitle(text) {
|
|
|
5
5
|
const title = firstLine.trim().slice(0, 80);
|
|
6
6
|
return title || 'Shennian';
|
|
7
7
|
}
|
|
8
|
+
export const CODEX_APP_SERVER_CLIENT_INFO = {
|
|
9
|
+
name: 'codex-tui',
|
|
10
|
+
title: 'Codex TUI',
|
|
11
|
+
version: '0.0.0',
|
|
12
|
+
};
|
|
8
13
|
const CODEX_COLLAB_AGENT_TOOL_NAMES = new Set([
|
|
9
14
|
'spawnAgent',
|
|
10
15
|
'sendInput',
|
|
@@ -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
|
@@ -6,7 +6,7 @@ import { AgentAdapter, registerAgent } from './adapter.js';
|
|
|
6
6
|
import { resolveBuiltinCommand, spawnAgentCommand } from './command-spec.js';
|
|
7
7
|
import { buildAgentProcessEnv } from '../agent-env.js';
|
|
8
8
|
import { ensurePlatformInstructionsFile } from './platform-instructions.js';
|
|
9
|
-
import { extractAppServerErrorMessage, formatCodexErrorMessage, formatTransientCodexStatus, isCodexCollabAgentItem, isCodexCollabAgentToolName, isCodexLegacyCollabAgentItem, isCodexUnsupportedEffortError, isMissingCodexRolloutError, isTransientCodexErrorMessage, looksLikeCodexInteractiveAuthPrompt, looksLikeFatalCodexStderr, makeThreadTitle, normalizeCodexModelId, normalizeCodexReasoningEffort, normalizeCodexStderr, normalizeTerminalText, safeStringify, stripGitDirectiveArtifacts, } from './codex-utils.js';
|
|
9
|
+
import { CODEX_APP_SERVER_CLIENT_INFO, extractAppServerErrorMessage, formatCodexErrorMessage, formatTransientCodexStatus, isCodexCollabAgentItem, isCodexCollabAgentToolName, isCodexLegacyCollabAgentItem, isCodexUnsupportedEffortError, isMissingCodexRolloutError, isTransientCodexErrorMessage, looksLikeCodexInteractiveAuthPrompt, looksLikeFatalCodexStderr, makeThreadTitle, normalizeCodexModelId, normalizeCodexReasoningEffort, normalizeCodexStderr, normalizeTerminalText, safeStringify, stripGitDirectiveArtifacts, } from './codex-utils.js';
|
|
10
10
|
export { isCodexUnsupportedEffortError, isMissingCodexRolloutError, normalizeCodexModelId, normalizeCodexReasoningEffort, } from './codex-utils.js';
|
|
11
11
|
function buildCodexTextInput(text, attachments) {
|
|
12
12
|
const images = attachments
|
|
@@ -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) {
|
|
@@ -239,7 +263,7 @@ export class CodexAdapter extends AgentAdapter {
|
|
|
239
263
|
async initializeAppServer(modelId) {
|
|
240
264
|
const codexModelId = normalizeCodexModelId(modelId);
|
|
241
265
|
await this.sendRpc('initialize', {
|
|
242
|
-
clientInfo:
|
|
266
|
+
clientInfo: CODEX_APP_SERVER_CLIENT_INFO,
|
|
243
267
|
capabilities: { experimentalApi: true },
|
|
244
268
|
});
|
|
245
269
|
if (this.agentSessionId) {
|
|
@@ -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))
|
|
@@ -6,6 +6,7 @@ import { fallbackClaudeAliasModels, discoverClaudeAliasModelsFromEnv, fallbackGe
|
|
|
6
6
|
import { runResolvedCommand } from './runner.js';
|
|
7
7
|
import { DISCOVERY_WORKDIR } from './types.js';
|
|
8
8
|
import { buildAgentProcessEnv, readLatestUserEnv } from '../../agent-env.js';
|
|
9
|
+
import { CODEX_APP_SERVER_CLIENT_INFO } from '../codex-utils.js';
|
|
9
10
|
function sendAppServerRpc(proc, pending, id, method, params, timeoutMs) {
|
|
10
11
|
if (!proc.stdin)
|
|
11
12
|
return Promise.reject(new Error('codex app-server stdin unavailable'));
|
|
@@ -59,7 +60,7 @@ async function discoverCodexModelsViaAppServer(spec) {
|
|
|
59
60
|
});
|
|
60
61
|
try {
|
|
61
62
|
await sendAppServerRpc(proc, pending, seq++, 'initialize', {
|
|
62
|
-
clientInfo:
|
|
63
|
+
clientInfo: CODEX_APP_SERVER_CLIENT_INFO,
|
|
63
64
|
capabilities: { experimentalApi: true },
|
|
64
65
|
}, 10_000);
|
|
65
66
|
const [modelList, configRead] = await Promise.all([
|
|
@@ -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;
|
|
@@ -42,9 +42,6 @@ export type ExternalChannelView = {
|
|
|
42
42
|
downloadAttachments?: boolean;
|
|
43
43
|
downloadAttachmentsDir?: string;
|
|
44
44
|
selfNickname?: string | null;
|
|
45
|
-
cloudOcrUrl?: string;
|
|
46
|
-
cloudOcrToken?: string;
|
|
47
|
-
cloudOcrMode?: string;
|
|
48
45
|
wechatRpaRuntimeState?: ExternalChannelSessionStatus['wechatRpaRuntimeState'];
|
|
49
46
|
wechatRpaLastRunAt?: string | null;
|
|
50
47
|
wechatRpaLastMessageAt?: string | null;
|
|
@@ -54,15 +51,9 @@ export type ExternalChannelView = {
|
|
|
54
51
|
wechatRpaLastTracePath?: string | null;
|
|
55
52
|
wechatRpaLastTraceSummary?: string | null;
|
|
56
53
|
wechatRpaRecentTaskSummaries?: ExternalChannelSessionStatus['wechatRpaRecentTaskSummaries'];
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
wechatRpaLastCloudOcrImageHash?: string | null;
|
|
61
|
-
wechatRpaLastCloudOcrUsage?: {
|
|
62
|
-
inputTokens?: number;
|
|
63
|
-
outputTokens?: number;
|
|
64
|
-
totalTokens?: number;
|
|
65
|
-
} | null;
|
|
54
|
+
wechatRpaPreflightChecks?: ExternalChannelSessionStatus['wechatRpaPreflightChecks'];
|
|
55
|
+
wechatRpaServerDecisionAvailable?: boolean | null;
|
|
56
|
+
wechatRpaPrivacyConsentAccepted?: boolean | null;
|
|
66
57
|
};
|
|
67
58
|
export type ExternalMessageEvent = {
|
|
68
59
|
type: 'external.message';
|
|
@@ -100,10 +100,8 @@ export declare class ChannelRuntime {
|
|
|
100
100
|
downloadAttachments?: boolean;
|
|
101
101
|
downloadAttachmentsDir?: string;
|
|
102
102
|
selfNickname?: string;
|
|
103
|
+
privacyConsentAccepted?: boolean;
|
|
103
104
|
flowScriptPath?: string;
|
|
104
|
-
cloudOcrUrl?: string;
|
|
105
|
-
cloudOcrToken?: string;
|
|
106
|
-
cloudOcrMode?: 'off' | 'fallback' | 'always';
|
|
107
105
|
}): Promise<ExternalChannelView>;
|
|
108
106
|
}
|
|
109
107
|
export type ExternalReplySendPlanItem = {
|
|
@@ -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'
|
|
@@ -357,7 +359,6 @@ export class ChannelRuntime {
|
|
|
357
359
|
: channel);
|
|
358
360
|
configs.push(nextConfig);
|
|
359
361
|
this.configs.replaceAll(configs);
|
|
360
|
-
const cloudOcrMode = 'off';
|
|
361
362
|
this.secrets.upsert(nextConfig.secretRef, {
|
|
362
363
|
type: 'wechat-rpa',
|
|
363
364
|
source,
|
|
@@ -370,10 +371,8 @@ export class ChannelRuntime {
|
|
|
370
371
|
downloadAttachments: input.downloadAttachments ?? (priorSecret?.downloadAttachments === undefined ? true : Boolean(priorSecret.downloadAttachments)),
|
|
371
372
|
downloadAttachmentsDir: input.downloadAttachmentsDir?.trim() || stringOrUndefined(priorSecret?.downloadAttachmentsDir),
|
|
372
373
|
selfNickname: input.selfNickname?.trim() || stringOrUndefined(priorSecret?.selfNickname),
|
|
374
|
+
privacyConsentAccepted: input.privacyConsentAccepted ?? Boolean(priorSecret?.privacyConsentAccepted),
|
|
373
375
|
flowScriptPath: input.flowScriptPath?.trim() || stringOrUndefined(priorSecret?.flowScriptPath),
|
|
374
|
-
cloudOcrUrl: '',
|
|
375
|
-
cloudOcrToken: '',
|
|
376
|
-
cloudOcrMode,
|
|
377
376
|
canReply: input.canReply ?? priorSecret?.canReply ?? false,
|
|
378
377
|
systemPrompt: input.systemPrompt ?? (typeof priorSecret?.systemPrompt === 'string' ? priorSecret.systemPrompt : ''),
|
|
379
378
|
});
|
|
@@ -414,6 +413,9 @@ function wechatRpaViewFields(secret) {
|
|
|
414
413
|
downloadAttachments: secret.downloadAttachments === undefined ? true : Boolean(secret.downloadAttachments),
|
|
415
414
|
downloadAttachmentsDir: typeof secret.downloadAttachmentsDir === 'string' ? secret.downloadAttachmentsDir : '',
|
|
416
415
|
selfNickname: typeof secret.selfNickname === 'string' ? secret.selfNickname : '',
|
|
416
|
+
wechatRpaPrivacyConsentAccepted: Boolean(secret.privacyConsentAccepted),
|
|
417
|
+
wechatRpaServerDecisionAvailable: true,
|
|
418
|
+
wechatRpaPreflightChecks: buildWeChatRpaPreflightChecks(secret),
|
|
417
419
|
};
|
|
418
420
|
}
|
|
419
421
|
function wechatRpaStatusFields(secret) {
|
|
@@ -430,6 +432,9 @@ function wechatRpaStatusFields(secret) {
|
|
|
430
432
|
downloadAttachments: secret.downloadAttachments === undefined ? true : Boolean(secret.downloadAttachments),
|
|
431
433
|
downloadAttachmentsDir: typeof secret.downloadAttachmentsDir === 'string' ? secret.downloadAttachmentsDir : null,
|
|
432
434
|
selfNickname: typeof secret.selfNickname === 'string' ? secret.selfNickname : null,
|
|
435
|
+
wechatRpaPrivacyConsentAccepted: Boolean(secret.privacyConsentAccepted),
|
|
436
|
+
wechatRpaServerDecisionAvailable: true,
|
|
437
|
+
wechatRpaPreflightChecks: buildWeChatRpaPreflightChecks(secret),
|
|
433
438
|
};
|
|
434
439
|
}
|
|
435
440
|
function normalizeWeChatRpaGroups(groups) {
|
|
@@ -445,7 +450,29 @@ function normalizeWeChatRpaGroups(groups) {
|
|
|
445
450
|
return result;
|
|
446
451
|
}
|
|
447
452
|
function defaultWeChatRpaSource() {
|
|
448
|
-
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;
|
|
449
476
|
}
|
|
450
477
|
export function planExternalReplySends(channelType, input) {
|
|
451
478
|
const parts = splitExternalReplyText(input.text);
|
|
@@ -17,13 +17,10 @@ type ChannelSecretRecord = {
|
|
|
17
17
|
downloadAttachments?: boolean;
|
|
18
18
|
downloadAttachmentsDir?: string;
|
|
19
19
|
selfNickname?: string;
|
|
20
|
+
privacyConsentAccepted?: boolean;
|
|
20
21
|
idleSeconds?: number;
|
|
21
22
|
recentLimit?: number;
|
|
22
|
-
readMode?: 'local-ocr' | 'hybrid-vlm';
|
|
23
23
|
flowScriptPath?: string;
|
|
24
|
-
cloudOcrUrl?: string;
|
|
25
|
-
cloudOcrToken?: string;
|
|
26
|
-
cloudOcrMode?: 'off' | 'fallback' | 'always';
|
|
27
24
|
updatedAt: string;
|
|
28
25
|
};
|
|
29
26
|
export declare class ChannelSecretRegistry {
|
|
@@ -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
|
+
}
|