work-ally 0.2.0-alpha.1

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.
Files changed (172) hide show
  1. package/AGENTS.md +110 -0
  2. package/DASHBOARD.md +160 -0
  3. package/PRODUCT.md +113 -0
  4. package/README.md +403 -0
  5. package/ally.sh +171 -0
  6. package/bridge/src/approval-rules.ts +360 -0
  7. package/bridge/src/channel-delivery.ts +207 -0
  8. package/bridge/src/channel-types.ts +22 -0
  9. package/bridge/src/channels/fake/adapter.ts +31 -0
  10. package/bridge/src/channels/feishu/adapter.ts +411 -0
  11. package/bridge/src/channels/feishu/approvals.ts +6 -0
  12. package/bridge/src/channels/feishu/formatter.ts +276 -0
  13. package/bridge/src/channels/feishu/normalize.ts +368 -0
  14. package/bridge/src/codex-config.ts +52 -0
  15. package/bridge/src/config.ts +240 -0
  16. package/bridge/src/fake-runtime-client.ts +505 -0
  17. package/bridge/src/handoff-service.ts +494 -0
  18. package/bridge/src/logger.ts +194 -0
  19. package/bridge/src/memory-digest.ts +186 -0
  20. package/bridge/src/receiver-approval-autonomy.ts +158 -0
  21. package/bridge/src/receiver-control-core.ts +140 -0
  22. package/bridge/src/receiver-control-work-session.ts +218 -0
  23. package/bridge/src/receiver-control.ts +83 -0
  24. package/bridge/src/receiver-delivery.ts +136 -0
  25. package/bridge/src/receiver-helpers.ts +96 -0
  26. package/bridge/src/receiver-human-gate.ts +333 -0
  27. package/bridge/src/receiver-inbound-preflight.ts +162 -0
  28. package/bridge/src/receiver-recovery.ts +236 -0
  29. package/bridge/src/receiver-runtime-callbacks.ts +367 -0
  30. package/bridge/src/receiver-runtime-policy.ts +132 -0
  31. package/bridge/src/receiver-runtime-state.ts +124 -0
  32. package/bridge/src/receiver-support-actions.ts +189 -0
  33. package/bridge/src/receiver-thread-start.ts +57 -0
  34. package/bridge/src/receiver-turn-coordination.ts +94 -0
  35. package/bridge/src/receiver-turn-execution.ts +257 -0
  36. package/bridge/src/receiver-turn-failure.ts +143 -0
  37. package/bridge/src/receiver-turn-result.ts +185 -0
  38. package/bridge/src/receiver-turn-steer.ts +70 -0
  39. package/bridge/src/receiver-work-session.ts +76 -0
  40. package/bridge/src/receiver.ts +329 -0
  41. package/bridge/src/router.ts +62 -0
  42. package/bridge/src/runtime-client-agent-messages.ts +150 -0
  43. package/bridge/src/runtime-client-message-dispatch.ts +176 -0
  44. package/bridge/src/runtime-client-protocol.ts +411 -0
  45. package/bridge/src/runtime-client-request-ops.ts +56 -0
  46. package/bridge/src/runtime-client-run-turn.ts +158 -0
  47. package/bridge/src/runtime-client-thread-ops.ts +270 -0
  48. package/bridge/src/runtime-client-transport.ts +309 -0
  49. package/bridge/src/runtime-client-turn-poll.ts +224 -0
  50. package/bridge/src/runtime-client-turn-read.ts +185 -0
  51. package/bridge/src/runtime-client-turn-state.ts +105 -0
  52. package/bridge/src/runtime-client.ts +344 -0
  53. package/bridge/src/runtime-user-input.ts +403 -0
  54. package/bridge/src/scheduler.ts +239 -0
  55. package/bridge/src/server-handoff-command.ts +364 -0
  56. package/bridge/src/server-main.ts +80 -0
  57. package/bridge/src/server-routine-command.ts +60 -0
  58. package/bridge/src/server-routine-execution.ts +222 -0
  59. package/bridge/src/server-runtime-app-support.ts +107 -0
  60. package/bridge/src/server-runtime-app.ts +238 -0
  61. package/bridge/src/server-thread-sync-command.ts +63 -0
  62. package/bridge/src/server.ts +17 -0
  63. package/bridge/src/session-store-delivery.ts +220 -0
  64. package/bridge/src/session-store-human-gate.ts +380 -0
  65. package/bridge/src/session-store-inbound-acceptance.ts +66 -0
  66. package/bridge/src/session-store-meta.ts +134 -0
  67. package/bridge/src/session-store-turn-ledger.ts +272 -0
  68. package/bridge/src/session-store.ts +380 -0
  69. package/bridge/src/system-notify.ts +220 -0
  70. package/bridge/src/thread-sync.ts +200 -0
  71. package/bridge/src/translator.ts +494 -0
  72. package/bridge/src/types.ts +289 -0
  73. package/bridge/src/utils.ts +104 -0
  74. package/bridge/src/work-session-store.ts +471 -0
  75. package/docs/.gitkeep +0 -0
  76. package/docs/architecture/codex-feishu-bridge-proposal.md +2742 -0
  77. package/docs/completed/FEATURE-feishu-markdown-and-reply-support.md +327 -0
  78. package/docs/completed/README.md +21 -0
  79. package/docs/completed/SPEC-approval-autonomy-and-safe-defaults.md +205 -0
  80. package/docs/completed/SPEC-approval-batch-and-strict-reply-shortcuts.md +153 -0
  81. package/docs/completed/SPEC-conversation-noise-reduction-and-busy-input-gate.md +538 -0
  82. package/docs/completed/SPEC-engineering-sop-skillization.md +190 -0
  83. package/docs/completed/SPEC-faithful-bridge-core-thinning-v2.md +376 -0
  84. package/docs/completed/SPEC-faithful-bridge-core-thinning.md +1071 -0
  85. package/docs/completed/SPEC-group-chat-sender-identity.md +301 -0
  86. package/docs/completed/SPEC-middleware-exception-visibility.md +227 -0
  87. package/docs/completed/SPEC-nightly-memory-digest-visibility.md +121 -0
  88. package/docs/completed/SPEC-project-group-chat-human-centered-conversation-mapping.md +326 -0
  89. package/docs/completed/SPEC-remove-cli-persona-bootstrap.md +201 -0
  90. package/docs/developer-workflow.md +49 -0
  91. package/docs/implementation/SPEC-codex-same-machine-session-handoff-implementation.md +239 -0
  92. package/docs/implementation/test-coverage-map.md +363 -0
  93. package/docs/implementation/work-ally-implementation-guide.md +790 -0
  94. package/docs/issues/README.md +10 -0
  95. package/docs/issues/pending/ANALYSIS-ally-premature-recovery-notice-and-task-state-semantics-2026-03-18.md +295 -0
  96. package/docs/issues/resolved/ANALYSIS-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md +466 -0
  97. package/docs/issues/resolved/ANALYSIS-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md +261 -0
  98. package/docs/issues/resolved/ANALYSIS-codex-app-server-transport-disconnect-semantics-2026-03-14.md +606 -0
  99. package/docs/issues/resolved/ANALYSIS-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md +348 -0
  100. package/docs/issues/resolved/ANALYSIS-runtime-turn-delivery-and-recovery-2026-03-14.md +603 -0
  101. package/docs/issues/resolved/ANALYSIS-self-test-gap-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md +166 -0
  102. package/docs/issues/resolved/ANALYSIS-self-test-gap-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md +186 -0
  103. package/docs/issues/resolved/ANALYSIS-self-test-gap-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md +166 -0
  104. package/docs/issues/resolved/REPORT-ally-runtime-turn-delivery-3b42fb8-2026-03-15.md +373 -0
  105. package/docs/manual-acceptance.md +127 -0
  106. package/docs/ops-runbook.md +44 -0
  107. package/docs/planning/FEATURE-memory-system.md +748 -0
  108. package/docs/planning/SPEC-active-turn-steer-and-context-compaction-visibility.md +269 -0
  109. package/docs/planning/SPEC-approval-rules-inheritance-and-local-validation-lane.md +450 -0
  110. package/docs/planning/SPEC-assistant-persona-bootstrap.md +199 -0
  111. package/docs/planning/SPEC-assistant-rename.md +610 -0
  112. package/docs/planning/SPEC-bridge-app-server-protocol-alignment.md +667 -0
  113. package/docs/planning/SPEC-claude-runtime-host-for-work-ally.md +434 -0
  114. package/docs/planning/SPEC-cli-feishu-codex-session-unification.md +236 -0
  115. package/docs/planning/SPEC-codex-same-machine-session-handoff.md +873 -0
  116. package/docs/planning/SPEC-feishu-reaction-shortcuts.md +282 -0
  117. package/docs/planning/SPEC-local-stable-release-boundary.md +166 -0
  118. package/docs/planning/SPEC-managed-thread-entry-and-surface-mobility.md +862 -0
  119. package/docs/planning/SPEC-minimal-bridge-semantics-and-user-visible-surface.md +362 -0
  120. package/docs/planning/SPEC-npm-alpha-distribution-and-install-first-release.md +222 -0
  121. package/docs/planning/SPEC-remove-websocket-runtime-transport.md +364 -0
  122. package/docs/planning/SPEC-runtime-abstraction-phase-1.md +424 -0
  123. package/docs/planning/SPEC-runtime-connection-and-turn-recovery-semantics.md +274 -0
  124. package/docs/planning/SPEC-session-presence-and-state-visibility.md +397 -0
  125. package/docs/planning/SPEC-skill-first-capability-packaging.md +338 -0
  126. package/docs/planning/SPEC-stable-archive-contract.md +456 -0
  127. package/docs/planning/SPEC-supervised-start-boundary.md +127 -0
  128. package/docs/planning/SPEC-user-barrier-reduction-and-activation.md +832 -0
  129. package/docs/planning/ally-next.md +1278 -0
  130. package/docs/planning/assistant-workbench-spec.md +725 -0
  131. package/docs/planning/product-workbench.md +283 -0
  132. package/docs/product-onboarding.md +227 -0
  133. package/docs/product-spec-standard.md +528 -0
  134. package/docs/troubleshooting.md +45 -0
  135. package/docs/user-quickstart.md +46 -0
  136. package/internal/dispatch.sh +95 -0
  137. package/internal/lib/common.sh +1450 -0
  138. package/internal/modules/assistant/manage.sh +1312 -0
  139. package/internal/modules/bootstrap/setup.sh +144 -0
  140. package/internal/modules/config/init-env.sh +10 -0
  141. package/internal/modules/global/manage.sh +154 -0
  142. package/internal/modules/handoff/manage.sh +54 -0
  143. package/internal/modules/mcp/manage.sh +83 -0
  144. package/internal/modules/ops/logs.sh +76 -0
  145. package/internal/modules/routines/manage.sh +55 -0
  146. package/internal/modules/runtime/assistant-autosave.sh +26 -0
  147. package/internal/modules/runtime/restart.sh +6 -0
  148. package/internal/modules/runtime/start.sh +283 -0
  149. package/internal/modules/runtime/status.sh +194 -0
  150. package/internal/modules/runtime/stop.sh +55 -0
  151. package/internal/modules/runtime/supervisor.sh +216 -0
  152. package/internal/modules/runtime/update.sh +26 -0
  153. package/package.json +41 -0
  154. package/runtime/config/.gitkeep +0 -0
  155. package/runtime/host/.gitkeep +0 -0
  156. package/runtime/host/healthcheck-codex-app-server.ts +22 -0
  157. package/runtime/host/ping-pong-codex-app-server.ts +66 -0
  158. package/runtime/host/probe-codex-app-server.ts +115 -0
  159. package/skills/archive-reader/SKILL.md +9 -0
  160. package/skills/feishu-production-debug/SKILL.md +37 -0
  161. package/skills/feishu-production-debug/references/feishu-debug-order.md +49 -0
  162. package/skills/feishu-production-debug/references/platform-permission-baseline.md +23 -0
  163. package/skills/issue-to-spec-triage/SKILL.md +44 -0
  164. package/skills/issue-to-spec-triage/references/triage-rules.md +66 -0
  165. package/skills/memory-digest/SKILL.md +9 -0
  166. package/skills/post-implementation-closure/SKILL.md +39 -0
  167. package/skills/post-implementation-closure/references/closure-checklist.md +45 -0
  168. package/skills/post-implementation-closure/references/doc-drift-map.md +49 -0
  169. package/skills/product-spec/SKILL.md +244 -0
  170. package/templates/env.example +5 -0
  171. package/templates/routines/nightly-memory-digest.yaml +10 -0
  172. package/templates/workspace/AGENTS.md +26 -0
@@ -0,0 +1,200 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import type { WorkAllyConfig } from './config.ts';
4
+ import { logInfo, logWarn } from './logger.ts';
5
+ import type { WorkSessionMeta } from './types.ts';
6
+ import { todayStamp, writeJsonFile } from './utils.ts';
7
+ import { WorkSessionStore } from './work-session-store.ts';
8
+
9
+ interface RuntimeThreadReader {
10
+ readThreadWithTurns(threadId: string): Promise<{ id: string; status: { type: string; activeFlags?: string[] } | null; turns: any[] }>;
11
+ }
12
+
13
+ interface SyncTurnMessage {
14
+ schema_version: number;
15
+ at: string;
16
+ day: string;
17
+ thread_id: string;
18
+ turn_id: string;
19
+ role: 'user' | 'assistant';
20
+ text: string;
21
+ source: 'thread_read';
22
+ }
23
+
24
+ interface SyncSummary {
25
+ syncedAt: string;
26
+ threadId: string;
27
+ resolvedThreadId: string;
28
+ status: string | null;
29
+ dayFiles: string[];
30
+ messageCount: number;
31
+ turnCount: number;
32
+ }
33
+
34
+ const SCHEMA_VERSION = 1;
35
+
36
+ function asText(value: unknown): string {
37
+ return typeof value === 'string' ? value.trim() : '';
38
+ }
39
+
40
+ function toIsoOrNow(value: unknown): string {
41
+ const text = asText(value);
42
+ if (!text) {
43
+ return new Date().toISOString();
44
+ }
45
+ const date = new Date(text);
46
+ if (Number.isNaN(date.getTime())) {
47
+ return new Date().toISOString();
48
+ }
49
+ return date.toISOString();
50
+ }
51
+
52
+ function collectUserAssistantMessages(threadId: string, turns: any[], timezone: string): { byDay: Map<string, SyncTurnMessage[]>; messageCount: number } {
53
+ const byDay = new Map<string, SyncTurnMessage[]>();
54
+ let messageCount = 0;
55
+
56
+ for (const turn of turns) {
57
+ const turnId = asText(turn?.id) || 'unknown-turn';
58
+ const items = Array.isArray(turn?.items) ? turn.items : [];
59
+ for (const item of items) {
60
+ const itemType = asText(item?.type);
61
+ if (itemType !== 'userMessage' && itemType !== 'agentMessage') {
62
+ continue;
63
+ }
64
+ const text = asText(item?.text);
65
+ if (!text) {
66
+ continue;
67
+ }
68
+ const at = toIsoOrNow(item?.createdAt || turn?.createdAt);
69
+ const day = todayStamp(new Date(at), timezone);
70
+ const role: 'user' | 'assistant' = itemType === 'userMessage' ? 'user' : 'assistant';
71
+ const entry: SyncTurnMessage = {
72
+ schema_version: SCHEMA_VERSION,
73
+ at,
74
+ day,
75
+ thread_id: threadId,
76
+ turn_id: turnId,
77
+ role,
78
+ text,
79
+ source: 'thread_read',
80
+ };
81
+ if (!byDay.has(day)) {
82
+ byDay.set(day, []);
83
+ }
84
+ byDay.get(day)?.push(entry);
85
+ messageCount += 1;
86
+ }
87
+ }
88
+
89
+ return { byDay, messageCount };
90
+ }
91
+
92
+ function writeDayThreadFile(archiveRoot: string, day: string, threadId: string, entries: SyncTurnMessage[]): string {
93
+ const [year, month, date] = day.split('-');
94
+ const dayDir = path.join(archiveRoot, year, month, date);
95
+ fs.mkdirSync(dayDir, { recursive: true });
96
+ const filePath = path.join(dayDir, `${threadId}.ndjson`);
97
+ const content = entries
98
+ .sort((left, right) => String(left.at).localeCompare(String(right.at)))
99
+ .map((entry) => JSON.stringify(entry))
100
+ .join('\n');
101
+ fs.writeFileSync(filePath, content ? `${content}\n` : '', 'utf8');
102
+ return filePath;
103
+ }
104
+
105
+ function isSyncEligible(session: WorkSessionMeta): boolean {
106
+ return !session.archivedAt && session.activeSurface !== 'closed' && Boolean(asText(session.runtimeThreadId));
107
+ }
108
+
109
+ export async function syncThreadConversationById(
110
+ config: WorkAllyConfig,
111
+ runtime: RuntimeThreadReader,
112
+ threadId: string,
113
+ ): Promise<SyncSummary | null> {
114
+ const targetThreadId = asText(threadId);
115
+ if (!targetThreadId) {
116
+ return null;
117
+ }
118
+
119
+ const snapshot = await runtime.readThreadWithTurns(targetThreadId);
120
+ const resolvedThreadId = asText(snapshot.id) || targetThreadId;
121
+ const status = asText(snapshot.status?.type) || null;
122
+ const turns = Array.isArray(snapshot.turns) ? snapshot.turns : [];
123
+
124
+ const { byDay, messageCount } = collectUserAssistantMessages(resolvedThreadId, turns, config.timezone);
125
+ if (byDay.size === 0) {
126
+ logInfo('thread_sync', 'thread_without_messages', {
127
+ threadId: resolvedThreadId,
128
+ requestedThreadId: targetThreadId,
129
+ });
130
+ return {
131
+ syncedAt: new Date().toISOString(),
132
+ threadId: targetThreadId,
133
+ resolvedThreadId,
134
+ status,
135
+ dayFiles: [],
136
+ messageCount: 0,
137
+ turnCount: turns.length,
138
+ };
139
+ }
140
+
141
+ const dayFiles: string[] = [];
142
+ for (const [day, entries] of byDay.entries()) {
143
+ const filePath = writeDayThreadFile(config.paths.archiveDir, day, resolvedThreadId, entries);
144
+ dayFiles.push(filePath);
145
+ }
146
+
147
+ const summary: SyncSummary = {
148
+ syncedAt: new Date().toISOString(),
149
+ threadId: targetThreadId,
150
+ resolvedThreadId,
151
+ status,
152
+ dayFiles: dayFiles.sort(),
153
+ messageCount,
154
+ turnCount: turns.length,
155
+ };
156
+
157
+ const summaryPath = path.join(config.paths.runtimeDir, 'thread-sync', `${resolvedThreadId}.json`);
158
+ writeJsonFile(summaryPath, summary);
159
+
160
+ logInfo('thread_sync', 'thread_synced', {
161
+ threadId: resolvedThreadId,
162
+ messageCount,
163
+ turnCount: turns.length,
164
+ dayFileCount: dayFiles.length,
165
+ });
166
+
167
+ return summary;
168
+ }
169
+
170
+ export async function syncAllActiveWorkSessions(
171
+ config: WorkAllyConfig,
172
+ runtime: RuntimeThreadReader,
173
+ workSessionStore: WorkSessionStore,
174
+ ): Promise<{ synced: number; skipped: number; failed: number }> {
175
+ const sessions = workSessionStore.listWorkSessions().filter(isSyncEligible);
176
+ let synced = 0;
177
+ let skipped = 0;
178
+ let failed = 0;
179
+
180
+ for (const session of sessions) {
181
+ const threadId = asText(session.runtimeThreadId);
182
+ if (!threadId) {
183
+ skipped += 1;
184
+ continue;
185
+ }
186
+ try {
187
+ await syncThreadConversationById(config, runtime, threadId);
188
+ synced += 1;
189
+ } catch (error) {
190
+ failed += 1;
191
+ logWarn('thread_sync', 'thread_sync_failed', {
192
+ threadId,
193
+ workSessionId: session.workSessionId,
194
+ error: error instanceof Error ? (error.stack || error.message) : String(error),
195
+ });
196
+ }
197
+ }
198
+
199
+ return { synced, skipped, failed };
200
+ }
@@ -0,0 +1,494 @@
1
+ import type {ApprovalRequest, ConnectionLifecycleEvent, PendingUserInputRecord, RuntimeThreadStatus, SessionMeta, WorkSessionMeta} from './types.ts';
2
+
3
+ interface ConnectionLifecycleMessageOptions {
4
+ hasActiveTurn?: boolean;
5
+ }
6
+
7
+ const SYSTEM_NOTICE_PREFIX_RE = /^\s*(?:\*\*)?【系统消息】(?:\*\*)?/;
8
+ const WORKING_NOTICE_PREFIX_RE =
9
+ /^\s*(?:\*\*)?(?:【工作中】|⏳ 工作中)(?:\*\*)?/;
10
+
11
+ export function systemNoticeMessage(text: string): string {
12
+ const trimmed = String(text || '').trim();
13
+ if (!trimmed) {
14
+ return '';
15
+ }
16
+ if (SYSTEM_NOTICE_PREFIX_RE.test(trimmed)) {
17
+ return trimmed;
18
+ }
19
+ return `**【系统消息】** ${trimmed}`;
20
+ }
21
+
22
+ export function workingNoticeMessage(text: string): string {
23
+ const trimmed = String(text || '').trim();
24
+ if (!trimmed) {
25
+ return '';
26
+ }
27
+ if (WORKING_NOTICE_PREFIX_RE.test(trimmed)) {
28
+ return trimmed;
29
+ }
30
+ return `**⏳ 工作中** ${trimmed}`;
31
+ }
32
+
33
+ function isMcpToolApprovalText(text: string|null|undefined): boolean {
34
+ return /MCP server wants to run the tool/i.test(text || '');
35
+ }
36
+
37
+ type WaitingUserInputSource =
38
+ Pick<PendingUserInputRecord, 'questions'|'elicitation'>;
39
+
40
+ function isMcpToolApprovalRequest(request: WaitingUserInputSource): boolean {
41
+ if (isMcpToolApprovalText(request.elicitation?.message)) {
42
+ return true;
43
+ }
44
+ if (request.questions.length !== 1) {
45
+ return false;
46
+ }
47
+ return isMcpToolApprovalText(request.questions[0]?.question);
48
+ }
49
+
50
+ function markdownSection(title: string, lines: string[]): string {
51
+ return [`**${title}**`, ...lines.map((line) => `- ${line}`)].join('\n');
52
+ }
53
+
54
+ function approvalKindLabel(kind: ApprovalRequest['kind']): string {
55
+ return kind === 'file_change' ? '文件修改' : '命令执行';
56
+ }
57
+
58
+ export function approvalMessage(request: ApprovalRequest): string {
59
+ const lines = [
60
+ '**审批请求**',
61
+ '- 先说明:这一步超出了默认自决边界,需要你明确拍板。',
62
+ `- 审批编号:${request.approvalId}`,
63
+ `- 类型:${approvalKindLabel(request.kind)}`,
64
+ ];
65
+
66
+ if (request.boundaryReason) {
67
+ lines.push(`- 边界原因:${request.boundaryReason}`);
68
+ }
69
+ if (request.command) {
70
+ lines.push(`- 动作:\`${request.command}\``);
71
+ }
72
+ if (request.grantRoot) {
73
+ lines.push(`- 影响范围:\`${request.grantRoot}\``);
74
+ }
75
+ if (request.cwd) {
76
+ lines.push(`- 工作目录:\`${request.cwd}\``);
77
+ }
78
+ if (request.reason) {
79
+ lines.push(`- 运行时原因:${request.reason}`);
80
+ }
81
+
82
+ lines.push(
83
+ '',
84
+ '**你现在可以这样处理**',
85
+ '- 当前会话正在等审批:直接回复 `OK` / `同意`,或 `NO` / `拒绝`',
86
+ `- 命令兜底:\`/approve ${request.approvalId}\` / \`/deny ${
87
+ request.approvalId}\``,
88
+ );
89
+ return lines.join('\n').trim();
90
+ }
91
+
92
+ export function batchedApprovalMessage(requests: ApprovalRequest[]): string {
93
+ const lines = [
94
+ `**审批请求(共 ${requests.length} 项)**`,
95
+ '- 这一轮有多项审批待处理。',
96
+ '- 回复 `OK` / `同意` 可全部通过;回复 `NO` / `拒绝` 可全部拒绝。',
97
+ '',
98
+ '**审批条目**',
99
+ ];
100
+
101
+ requests.forEach((request, index) => {
102
+ const parts = [`${index + 1}. ${approvalKindLabel(request.kind)}`];
103
+ if (request.command) {
104
+ parts.push(`动作:\`${request.command}\``);
105
+ }
106
+ if (request.grantRoot) {
107
+ parts.push(`影响范围:\`${request.grantRoot}\``);
108
+ }
109
+ if (request.cwd) {
110
+ parts.push(`工作目录:\`${request.cwd}\``);
111
+ }
112
+ if (request.reason) {
113
+ parts.push(`运行时原因:${request.reason}`);
114
+ }
115
+ lines.push(`- ${parts.join(';')}`);
116
+ });
117
+
118
+ lines.push(
119
+ '',
120
+ '**你现在可以这样处理**',
121
+ '- 当前会话正在等审批:直接回复 `OK` / `同意`,或 `NO` / `拒绝`',
122
+ );
123
+
124
+ return lines.join('\n').trim();
125
+ }
126
+
127
+ function previewLine(label: string, value: string|null|undefined): string {
128
+ if (!value) {
129
+ return `${label}: none`;
130
+ }
131
+ const compact = value.replace(/\s+/g, ' ').trim();
132
+ if (compact.length <= 120) {
133
+ return `${label}: ${compact}`;
134
+ }
135
+ return `${label}: ${compact.slice(0, 120)}…`;
136
+ }
137
+
138
+ export function userInputMessage(request: PendingUserInputRecord): string {
139
+ const lines = [
140
+ isMcpToolApprovalRequest(request) ?
141
+ '需要你确认一次 MCP 工具调用后我才能继续。' :
142
+ '需要你补充信息后我才能继续。',
143
+ request.questions.length > 1 ? '请按 `问题ID: 回答` 的格式逐行回复。' :
144
+ '请直接回复答案即可。',
145
+ ];
146
+
147
+ for (const [index, question] of request.questions.entries()) {
148
+ lines.push('');
149
+ lines.push(`${index + 1}. ${question.header || question.id}`);
150
+ lines.push(`question_id: ${question.id}`);
151
+ lines.push(question.question);
152
+ if (question.options?.length) {
153
+ lines.push(`可选项:${
154
+ question.options.map((option) => option.label).join(' / ')}`);
155
+ }
156
+ if (question.isSecret) {
157
+ lines.push('如涉及敏感信息,请只回复必要部分。');
158
+ }
159
+ }
160
+
161
+ return lines.join('\n').trim();
162
+ }
163
+
164
+ export function connectionLifecycleMessage(
165
+ event: ConnectionLifecycleEvent,
166
+ options: ConnectionLifecycleMessageOptions = {}): string {
167
+ const sourceLabel =
168
+ event.source === 'runtime' ? 'Codex runtime' : 'Feishu 长连接';
169
+ const hasActiveTurn = Boolean(options.hasActiveTurn);
170
+
171
+ if (event.state === 'reconnected') {
172
+ if (event.source === 'runtime' && hasActiveTurn) {
173
+ return `${sourceLabel} 连接已恢复,继续确认这一轮结果。`;
174
+ }
175
+ return event.source === 'runtime' ? `${sourceLabel} 连接已恢复。` :
176
+ `${sourceLabel} 已恢复。`;
177
+ }
178
+
179
+ if (event.state === 'reconnecting') {
180
+ if (event.source === 'runtime') {
181
+ const attemptText = event.attempt ? `第 ${event.attempt} 次` : '本轮';
182
+ const delayText = event.nextRetryDelayMs ?
183
+ `,约 ${
184
+ Math.max(1, Math.ceil(event.nextRetryDelayMs / 1000))} 秒后重试` :
185
+ '';
186
+ if (hasActiveTurn) {
187
+ return `${sourceLabel} 正在重连,并继续确认这一轮结果(${attemptText}${
188
+ delayText})。`;
189
+ }
190
+ return `${sourceLabel} 正在重连(${attemptText}${delayText})。`;
191
+ }
192
+ return `${sourceLabel} 正在自动重连。`;
193
+ }
194
+
195
+ if (event.source === 'runtime' && hasActiveTurn) {
196
+ return `${sourceLabel} 已断开,正在继续确认这一轮结果。`;
197
+ }
198
+
199
+ const reason = String(event.reason || '').trim();
200
+ return reason ? `${sourceLabel} 已断开(${reason})。` :
201
+ `${sourceLabel} 已断开。`;
202
+ }
203
+
204
+ export function runtimeRecoveryRequiredMessage(): string {
205
+ return '系统暂时还没确认这一轮是否已完成。为避免漏掉结果,请重新发送上一条消息。';
206
+ }
207
+
208
+ export function runtimeFinalStateRecoveryRequiredMessage(): string {
209
+ return '系统暂时还没确认这一轮是否已完成。为避免漏掉结果,请重新发送上一条消息。';
210
+ }
211
+
212
+ export function runtimeInfrastructureErrorMessage(): string {
213
+ return '系统暂时还没确认这一轮是否已完成。若长时间无恢复,请重新发送上一条消息。';
214
+ }
215
+
216
+ export function pendingDeliveryIssueMessage(): string {
217
+ return '上一轮已有结果或状态产出,但消息当时没能稳定送达;我会先尝试补发上一轮结果,再继续处理你刚发的这条。';
218
+ }
219
+
220
+ export function contextCompactionStatusMessage(): string {
221
+ return systemNoticeMessage('Codex 正在压缩当前会话上下文。');
222
+ }
223
+
224
+ export function redeliveredPreviousTurnPrefixMessage(): string {
225
+ return '补发上一轮未稳定送达的结果:';
226
+ }
227
+ export function busyNewThreadControlMessage(): string {
228
+ return systemNoticeMessage(
229
+ '当前仍在处理上一条;如需新开会话,请先发送 /stop。');
230
+ }
231
+
232
+ export function newWorkSessionCreatedMessage(): string {
233
+ return '已新建并切换到新的工作线程;后续消息会继续这条新 thread。';
234
+ }
235
+
236
+ export function workSessionTakenOverMessage(): string {
237
+ return '已接回当前工作线程;后续消息会继续同一条 Codex thread。';
238
+ }
239
+
240
+ export function workSessionAlreadyOnChannelMessage(): string {
241
+ return systemNoticeMessage('当前工作线程已经在这里继续,无需重复接回。');
242
+ }
243
+
244
+ export function noLinkedWorkSessionMessage(): string {
245
+ return systemNoticeMessage(
246
+ '当前没有已链接的工作线程;如果你要接回官方 Codex 里已在跑的线程,请先在本机执行 `ally attach --assistant <assistant> --last`。');
247
+ }
248
+
249
+ export function noLinkedOrAttachedWorkSessionMessage(): string {
250
+ return systemNoticeMessage(
251
+ '当前没有已链接的工作线程;如果这条线程是你先在官方 Codex CLI 里开的,请先在本机执行 `ally attach --assistant <assistant> --last`,attach 成功后再回来发送 /takeover。');
252
+ }
253
+
254
+ function threadPreview(session: Pick<WorkSessionMeta, 'threadName' | 'lastPromptPreview' | 'lastReplyPreview'>): string {
255
+ const preview = [session.threadName, session.lastPromptPreview, session.lastReplyPreview]
256
+ .map((value) => String(value || '').trim())
257
+ .find((value) => value.length > 0);
258
+ if (!preview) {
259
+ return '无标题';
260
+ }
261
+ return preview.replace(/\s+/g, ' ').slice(0, 60);
262
+ }
263
+
264
+ export function workSessionThreadsMessage(
265
+ sessions: WorkSessionMeta[],
266
+ options: {
267
+ highlightedWorkSessionId?: string | null;
268
+ } = {},
269
+ ): string {
270
+ if (sessions.length === 0) {
271
+ return systemNoticeMessage('当前还没有可接回的受管线程。');
272
+ }
273
+
274
+ const lines = ['**当前可接回的线程**'];
275
+ for (const session of sessions) {
276
+ const badges = [];
277
+ if (options.highlightedWorkSessionId && session.workSessionId === options.highlightedWorkSessionId) {
278
+ badges.push('default');
279
+ }
280
+ if (session.origin === 'codex_cli_attach') {
281
+ badges.push('attach');
282
+ }
283
+ const suffix = badges.length ? ` (${badges.join(', ')})` : '';
284
+ lines.push(`- \`${session.threadHandle}\` ${threadPreview(session)}${suffix}`);
285
+ lines.push(` surface: ${session.activeSurface}; updated: ${session.updatedAt}`);
286
+ }
287
+ return lines.join('\n');
288
+ }
289
+
290
+ export function ambiguousTakeoverMessage(sessions: WorkSessionMeta[]): string {
291
+ return systemNoticeMessage([
292
+ `当前有 ${sessions.length} 条可接回的受管线程,系统不会替你猜。`,
293
+ '先发送 `/threads` 查看 handle,再发送 `/takeover <thread-handle>`。',
294
+ ].join('\n'));
295
+ }
296
+
297
+ export function takeoverHandleNotFoundMessage(threadHandle: string): string {
298
+ return systemNoticeMessage(`找不到线程句柄 \`${threadHandle}\`;先发送 /threads 看当前可选项。`);
299
+ }
300
+
301
+ export function codexBindingRequiredMessage(): string {
302
+ return systemNoticeMessage('当前会话还没有绑定受管线程;先发普通消息、`/new`,或先 `/threads` 再 `/takeover <thread-handle>`。');
303
+ }
304
+
305
+ export function codexHandoffMessage(payload: {
306
+ status: 'ready'|'not_ready'|'not_found';
307
+ reason?: 'active_work_session_missing' | 'cli_resume_ref_missing';
308
+ assistantName: string;
309
+ workspaceRoot: string;
310
+ assistantCodexHome: string | null;
311
+ workSessionId: string | null;
312
+ runtimeThreadId: string | null;
313
+ cliResumeRef: string | null;
314
+ cliResumeRefType: 'session_id' | 'thread_name' | 'unknown' | 'null';
315
+ threadName: string | null;
316
+ activeSurface: string | null;
317
+ ownershipSource: string | null;
318
+ resumeCapability: 'ready' | 'not_ready';
319
+ command: string | null;
320
+ exportedAt: string;
321
+ }): string {
322
+ const stablePayload = {
323
+ status: payload.status,
324
+ reason: payload.reason || null,
325
+ assistant: payload.assistantName,
326
+ workspace_root: payload.workspaceRoot,
327
+ assistant_codex_home: payload.assistantCodexHome,
328
+ work_session_id: payload.workSessionId,
329
+ runtime_thread_id: payload.runtimeThreadId,
330
+ cli_resume_ref: payload.cliResumeRef,
331
+ cli_resume_ref_type: payload.cliResumeRefType,
332
+ thread_name: payload.threadName,
333
+ active_surface: payload.activeSurface,
334
+ ownership_source: payload.ownershipSource,
335
+ resume_capability: payload.resumeCapability,
336
+ command: payload.command,
337
+ exported_at: payload.exportedAt,
338
+ };
339
+
340
+ if (payload.status === 'ready' && payload.command) {
341
+ return [
342
+ '在电脑的命令行执行下面命令,可以继续使用 codex-cli 进行当前会话:',
343
+ '```bash',
344
+ payload.command,
345
+ '```',
346
+ ].join('\n');
347
+ }
348
+
349
+ if (payload.status === 'not_found') {
350
+ return systemNoticeMessage([
351
+ '当前没有可接续的工作线程;/codex 只会导出当前工作线程,不会替你隐式新开会话。',
352
+ '',
353
+ '**handoff payload**',
354
+ '```json',
355
+ JSON.stringify(stablePayload, null, 2),
356
+ '```',
357
+ ].join('\n'));
358
+ }
359
+
360
+ return systemNoticeMessage([
361
+ '当前工作线程还没有官方 Codex CLI 可恢复句柄,所以现在只能确认 runtime thread 还在,但还不能给出 `codex resume ...`。',
362
+ '这不是新开会话失败,而是 `cliResumeRef` 还没准备好;系统不会拿 `runtimeThreadId` 冒充官方 CLI resume 句柄。',
363
+ '',
364
+ '**handoff payload**',
365
+ '```json',
366
+ JSON.stringify(stablePayload, null, 2),
367
+ '```',
368
+ ].join('\n'));
369
+ }
370
+
371
+ export function unsupportedHandoffCommandInGroupMessage(
372
+ command: '/takeover'|'/codex'): string {
373
+ return systemNoticeMessage(`群聊暂不支持 ${command};请改用单聊入口继续。`);
374
+ }
375
+
376
+ export function workSessionOwnershipRejectedMessage(
377
+ surface: 'official_codex_cli'|'idle'|'closed'): string {
378
+ if (surface === 'official_codex_cli') {
379
+ return systemNoticeMessage(
380
+ '当前工作线程正在官方 Codex CLI 中继续;如需切回这里,请先发送 /takeover,或发送 /new 新开一条工作线程。');
381
+ }
382
+ if (surface === 'idle') {
383
+ return systemNoticeMessage(
384
+ '当前工作线程的 owner 尚未明确;请先发送 /takeover 接回,或发送 /new 新开一条工作线程。');
385
+ }
386
+ return systemNoticeMessage(
387
+ '当前工作线程已关闭,不能继续写入;如需继续,请发送 /new 新开一条工作线程。');
388
+ }
389
+ export function runningStatusMessage(): string {
390
+ return '正在处理,请稍候。';
391
+ }
392
+
393
+ export function completionWithoutReplyStatusMessage(): string {
394
+ return systemNoticeMessage(
395
+ '本轮已结束,当前没有更多动作;如需继续,请直接发下一条消息。');
396
+ }
397
+
398
+ export function waitingApprovalStatusMessage(): string {
399
+ return '当前已暂停,等待你审批后继续。';
400
+ }
401
+
402
+ export function waitingApprovalFollowUpMessage(): string {
403
+ return '当前有待处理审批。请回复 OK / 同意,或 NO / 拒绝。';
404
+ }
405
+
406
+ export function waitingUserInputStatusMessage(request: WaitingUserInputSource):
407
+ string {
408
+ return isMcpToolApprovalRequest(request) ? '当前已暂停,等待你确认后继续。' :
409
+ '当前已暂停,等待你回复后继续。';
410
+ }
411
+
412
+ export function waitingUserInputFollowUpMessage(): string {
413
+ return '当前这轮已暂停,正在等你补充信息;你刚发的这条我还没转给 Codex。先回答当前问题,再继续发下一条。';
414
+ }
415
+
416
+ export function activeTurnNotReadyFollowUpMessage(): string {
417
+ return '当前这一轮暂时还不能接收新消息;你刚发的这条我先没转给 Codex,请稍后再试。';
418
+ }
419
+
420
+ export function heartbeatMessage(session: SessionMeta): string {
421
+ if (session.pendingApprovalIds.length > 0) {
422
+ const firstApprovalId = session.pendingApprovalIds[0];
423
+ if (session.pendingApprovalIds.length === 1) {
424
+ return `仍在等待你的审批:直接回复 OK / 同意,或 NO / 拒绝;也可输入 /approve ${
425
+ firstApprovalId} / /deny ${firstApprovalId}`;
426
+ }
427
+ return `仍在等待你的审批,当前共有 ${
428
+ session.pendingApprovalIds
429
+ .length} 项。请直接回复 OK / 同意,或 NO / 拒绝。`;
430
+ }
431
+
432
+ if (session.pendingUserInputRequestId) {
433
+ return '仍在等待你的补充信息,请直接回复即可。';
434
+ }
435
+
436
+ return '正在思考中,请稍候…';
437
+ }
438
+
439
+ export function statusMessage(
440
+ session: SessionMeta,
441
+ runtimeStatus: RuntimeThreadStatus|null = null,
442
+ assistant?: {
443
+ mode: 'assistant'|'native';
444
+ name?: string;
445
+ assistantHome?: string;
446
+ codexHome?: string;
447
+ },
448
+ ): string {
449
+ const thread =
450
+ session.threadId ? `thread: ${session.threadId}` : 'thread: (none yet)';
451
+ const runtimeState = session.threadId ?
452
+ `runtime status: ${runtimeStatus?.type || 'unknown'}` :
453
+ 'runtime status: (no thread)';
454
+ const runtimeFlags = runtimeStatus?.activeFlags?.length ?
455
+ `runtime flags: ${runtimeStatus.activeFlags.join(', ')}` :
456
+ 'runtime flags: none';
457
+ const activeTurn = session.activeTurnId ?
458
+ `active turn: ${session.activeTurnId}` :
459
+ 'active turn: none';
460
+ const lastTurn = session.lastTurnId ? `last turn: ${session.lastTurnId}` :
461
+ 'last turn: none';
462
+ const approvals = session.pendingApprovalIds.length ?
463
+ `pending approvals: ${session.pendingApprovalIds.join(', ')}` :
464
+ 'pending approvals: none';
465
+ const pendingUserInput = session.pendingUserInputRequestId ?
466
+ `pending user input: ${session.pendingUserInputRequestId}` :
467
+ 'pending user input: none';
468
+ const updatedAt = `updated at: ${session.updatedAt}`;
469
+ const lastMessage = session.lastMessageId ?
470
+ `last message: ${session.lastMessageId}` :
471
+ 'last message: none';
472
+
473
+ return [
474
+ markdownSection(
475
+ 'Mode',
476
+ [
477
+ `mode: ${assistant?.mode || 'native'}`,
478
+ `assistant: ${assistant?.name || 'none'}`,
479
+ `assistant_home: ${assistant?.assistantHome || 'none'}`,
480
+ `codex_home: ${assistant?.codexHome || 'none'}`,
481
+ ]),
482
+ markdownSection('Thread', [thread, runtimeState, runtimeFlags]),
483
+ markdownSection('Turn', [activeTurn, lastTurn]),
484
+ markdownSection('Queue', [approvals, pendingUserInput]),
485
+ markdownSection(
486
+ 'Recent',
487
+ [
488
+ lastMessage,
489
+ previewLine('last prompt', session.lastPromptPreview),
490
+ previewLine('last reply', session.lastReplyPreview),
491
+ updatedAt,
492
+ ]),
493
+ ].join('\n\n');
494
+ }