feique 1.1.0

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 (227) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +220 -0
  3. package/README.md +265 -0
  4. package/dist/backend/claude.d.ts +36 -0
  5. package/dist/backend/claude.js +358 -0
  6. package/dist/backend/claude.js.map +1 -0
  7. package/dist/backend/codex.d.ts +31 -0
  8. package/dist/backend/codex.js +100 -0
  9. package/dist/backend/codex.js.map +1 -0
  10. package/dist/backend/factory.d.ts +9 -0
  11. package/dist/backend/factory.js +56 -0
  12. package/dist/backend/factory.js.map +1 -0
  13. package/dist/backend/types.d.ts +54 -0
  14. package/dist/backend/types.js +2 -0
  15. package/dist/backend/types.js.map +1 -0
  16. package/dist/bridge/commands.d.ts +135 -0
  17. package/dist/bridge/commands.js +860 -0
  18. package/dist/bridge/commands.js.map +1 -0
  19. package/dist/bridge/service.d.ts +160 -0
  20. package/dist/bridge/service.js +3785 -0
  21. package/dist/bridge/service.js.map +1 -0
  22. package/dist/bridge/task-queue.d.ts +14 -0
  23. package/dist/bridge/task-queue.js +81 -0
  24. package/dist/bridge/task-queue.js.map +1 -0
  25. package/dist/bridge/types.d.ts +39 -0
  26. package/dist/bridge/types.js +2 -0
  27. package/dist/bridge/types.js.map +1 -0
  28. package/dist/cli.d.ts +2 -0
  29. package/dist/cli.js +1199 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/codex/capabilities.d.ts +20 -0
  32. package/dist/codex/capabilities.js +41 -0
  33. package/dist/codex/capabilities.js.map +1 -0
  34. package/dist/codex/runner.d.ts +47 -0
  35. package/dist/codex/runner.js +294 -0
  36. package/dist/codex/runner.js.map +1 -0
  37. package/dist/codex/session-index.d.ts +22 -0
  38. package/dist/codex/session-index.js +205 -0
  39. package/dist/codex/session-index.js.map +1 -0
  40. package/dist/collaboration/awareness.d.ts +36 -0
  41. package/dist/collaboration/awareness.js +107 -0
  42. package/dist/collaboration/awareness.js.map +1 -0
  43. package/dist/collaboration/digest.d.ts +65 -0
  44. package/dist/collaboration/digest.js +178 -0
  45. package/dist/collaboration/digest.js.map +1 -0
  46. package/dist/collaboration/handoff.d.ts +66 -0
  47. package/dist/collaboration/handoff.js +94 -0
  48. package/dist/collaboration/handoff.js.map +1 -0
  49. package/dist/collaboration/insights.d.ts +24 -0
  50. package/dist/collaboration/insights.js +243 -0
  51. package/dist/collaboration/insights.js.map +1 -0
  52. package/dist/collaboration/knowledge.d.ts +26 -0
  53. package/dist/collaboration/knowledge.js +105 -0
  54. package/dist/collaboration/knowledge.js.map +1 -0
  55. package/dist/collaboration/timeline.d.ts +31 -0
  56. package/dist/collaboration/timeline.js +150 -0
  57. package/dist/collaboration/timeline.js.map +1 -0
  58. package/dist/collaboration/trust.d.ts +49 -0
  59. package/dist/collaboration/trust.js +176 -0
  60. package/dist/collaboration/trust.js.map +1 -0
  61. package/dist/config/codex-skill.d.ts +7 -0
  62. package/dist/config/codex-skill.js +44 -0
  63. package/dist/config/codex-skill.js.map +1 -0
  64. package/dist/config/doctor.d.ts +12 -0
  65. package/dist/config/doctor.js +314 -0
  66. package/dist/config/doctor.js.map +1 -0
  67. package/dist/config/init.d.ts +3 -0
  68. package/dist/config/init.js +123 -0
  69. package/dist/config/init.js.map +1 -0
  70. package/dist/config/load.d.ts +33 -0
  71. package/dist/config/load.js +252 -0
  72. package/dist/config/load.js.map +1 -0
  73. package/dist/config/mutate.d.ts +21 -0
  74. package/dist/config/mutate.js +86 -0
  75. package/dist/config/mutate.js.map +1 -0
  76. package/dist/config/paths.d.ts +3 -0
  77. package/dist/config/paths.js +33 -0
  78. package/dist/config/paths.js.map +1 -0
  79. package/dist/config/schema.d.ts +308 -0
  80. package/dist/config/schema.js +250 -0
  81. package/dist/config/schema.js.map +1 -0
  82. package/dist/control-plane/project-session.d.ts +67 -0
  83. package/dist/control-plane/project-session.js +234 -0
  84. package/dist/control-plane/project-session.js.map +1 -0
  85. package/dist/feishu/base.d.ts +19 -0
  86. package/dist/feishu/base.js +93 -0
  87. package/dist/feishu/base.js.map +1 -0
  88. package/dist/feishu/cards.d.ts +22 -0
  89. package/dist/feishu/cards.js +144 -0
  90. package/dist/feishu/cards.js.map +1 -0
  91. package/dist/feishu/client.d.ts +61 -0
  92. package/dist/feishu/client.js +315 -0
  93. package/dist/feishu/client.js.map +1 -0
  94. package/dist/feishu/diagnostics.d.ts +42 -0
  95. package/dist/feishu/diagnostics.js +194 -0
  96. package/dist/feishu/diagnostics.js.map +1 -0
  97. package/dist/feishu/doc.d.ts +13 -0
  98. package/dist/feishu/doc.js +59 -0
  99. package/dist/feishu/doc.js.map +1 -0
  100. package/dist/feishu/extractors.d.ts +7 -0
  101. package/dist/feishu/extractors.js +215 -0
  102. package/dist/feishu/extractors.js.map +1 -0
  103. package/dist/feishu/long-connection.d.ts +12 -0
  104. package/dist/feishu/long-connection.js +41 -0
  105. package/dist/feishu/long-connection.js.map +1 -0
  106. package/dist/feishu/message-resource.d.ts +14 -0
  107. package/dist/feishu/message-resource.js +309 -0
  108. package/dist/feishu/message-resource.js.map +1 -0
  109. package/dist/feishu/replay.d.ts +37 -0
  110. package/dist/feishu/replay.js +114 -0
  111. package/dist/feishu/replay.js.map +1 -0
  112. package/dist/feishu/task.d.ts +18 -0
  113. package/dist/feishu/task.js +86 -0
  114. package/dist/feishu/task.js.map +1 -0
  115. package/dist/feishu/text.d.ts +23 -0
  116. package/dist/feishu/text.js +155 -0
  117. package/dist/feishu/text.js.map +1 -0
  118. package/dist/feishu/webhook.d.ts +23 -0
  119. package/dist/feishu/webhook.js +130 -0
  120. package/dist/feishu/webhook.js.map +1 -0
  121. package/dist/feishu/wiki.d.ts +52 -0
  122. package/dist/feishu/wiki.js +300 -0
  123. package/dist/feishu/wiki.js.map +1 -0
  124. package/dist/index.d.ts +9 -0
  125. package/dist/index.js +9 -0
  126. package/dist/index.js.map +1 -0
  127. package/dist/knowledge/search.d.ts +11 -0
  128. package/dist/knowledge/search.js +83 -0
  129. package/dist/knowledge/search.js.map +1 -0
  130. package/dist/logging.d.ts +3 -0
  131. package/dist/logging.js +40 -0
  132. package/dist/logging.js.map +1 -0
  133. package/dist/mcp/server.d.ts +34 -0
  134. package/dist/mcp/server.js +1196 -0
  135. package/dist/mcp/server.js.map +1 -0
  136. package/dist/memory/embedding-factory.d.ts +6 -0
  137. package/dist/memory/embedding-factory.js +20 -0
  138. package/dist/memory/embedding-factory.js.map +1 -0
  139. package/dist/memory/embeddings.d.ts +40 -0
  140. package/dist/memory/embeddings.js +150 -0
  141. package/dist/memory/embeddings.js.map +1 -0
  142. package/dist/memory/ollama-embeddings.d.ts +63 -0
  143. package/dist/memory/ollama-embeddings.js +215 -0
  144. package/dist/memory/ollama-embeddings.js.map +1 -0
  145. package/dist/memory/retrieve.d.ts +17 -0
  146. package/dist/memory/retrieve.js +29 -0
  147. package/dist/memory/retrieve.js.map +1 -0
  148. package/dist/memory/summarize.d.ts +13 -0
  149. package/dist/memory/summarize.js +58 -0
  150. package/dist/memory/summarize.js.map +1 -0
  151. package/dist/observability/cost.d.ts +12 -0
  152. package/dist/observability/cost.js +22 -0
  153. package/dist/observability/cost.js.map +1 -0
  154. package/dist/observability/dashboard-html.d.ts +5 -0
  155. package/dist/observability/dashboard-html.js +304 -0
  156. package/dist/observability/dashboard-html.js.map +1 -0
  157. package/dist/observability/metrics.d.ts +36 -0
  158. package/dist/observability/metrics.js +230 -0
  159. package/dist/observability/metrics.js.map +1 -0
  160. package/dist/observability/readiness.d.ts +31 -0
  161. package/dist/observability/readiness.js +57 -0
  162. package/dist/observability/readiness.js.map +1 -0
  163. package/dist/observability/server.d.ts +84 -0
  164. package/dist/observability/server.js +181 -0
  165. package/dist/observability/server.js.map +1 -0
  166. package/dist/projects/paths.d.ts +9 -0
  167. package/dist/projects/paths.js +30 -0
  168. package/dist/projects/paths.js.map +1 -0
  169. package/dist/runtime/instance-lock.d.ts +12 -0
  170. package/dist/runtime/instance-lock.js +99 -0
  171. package/dist/runtime/instance-lock.js.map +1 -0
  172. package/dist/runtime/process.d.ts +2 -0
  173. package/dist/runtime/process.js +43 -0
  174. package/dist/runtime/process.js.map +1 -0
  175. package/dist/runtime/shutdown.d.ts +11 -0
  176. package/dist/runtime/shutdown.js +38 -0
  177. package/dist/runtime/shutdown.js.map +1 -0
  178. package/dist/security/access.d.ts +13 -0
  179. package/dist/security/access.js +160 -0
  180. package/dist/security/access.js.map +1 -0
  181. package/dist/service/install.d.ts +19 -0
  182. package/dist/service/install.js +35 -0
  183. package/dist/service/install.js.map +1 -0
  184. package/dist/service/templates.d.ts +22 -0
  185. package/dist/service/templates.js +118 -0
  186. package/dist/service/templates.js.map +1 -0
  187. package/dist/state/audit-log.d.ts +33 -0
  188. package/dist/state/audit-log.js +116 -0
  189. package/dist/state/audit-log.js.map +1 -0
  190. package/dist/state/config-history-store.d.ts +27 -0
  191. package/dist/state/config-history-store.js +65 -0
  192. package/dist/state/config-history-store.js.map +1 -0
  193. package/dist/state/handoff-store.d.ts +20 -0
  194. package/dist/state/handoff-store.js +97 -0
  195. package/dist/state/handoff-store.js.map +1 -0
  196. package/dist/state/idempotency-store.d.ts +19 -0
  197. package/dist/state/idempotency-store.js +84 -0
  198. package/dist/state/idempotency-store.js.map +1 -0
  199. package/dist/state/memory-store.d.ts +137 -0
  200. package/dist/state/memory-store.js +713 -0
  201. package/dist/state/memory-store.js.map +1 -0
  202. package/dist/state/pending-command-store.d.ts +30 -0
  203. package/dist/state/pending-command-store.js +108 -0
  204. package/dist/state/pending-command-store.js.map +1 -0
  205. package/dist/state/run-state-store.d.ts +58 -0
  206. package/dist/state/run-state-store.js +269 -0
  207. package/dist/state/run-state-store.js.map +1 -0
  208. package/dist/state/session-store.d.ts +56 -0
  209. package/dist/state/session-store.js +275 -0
  210. package/dist/state/session-store.js.map +1 -0
  211. package/dist/state/trust-store.d.ts +15 -0
  212. package/dist/state/trust-store.js +53 -0
  213. package/dist/state/trust-store.js.map +1 -0
  214. package/dist/utils/fs.d.ts +4 -0
  215. package/dist/utils/fs.js +26 -0
  216. package/dist/utils/fs.js.map +1 -0
  217. package/dist/utils/json.d.ts +1 -0
  218. package/dist/utils/json.js +9 -0
  219. package/dist/utils/json.js.map +1 -0
  220. package/dist/utils/path.d.ts +3 -0
  221. package/dist/utils/path.js +22 -0
  222. package/dist/utils/path.js.map +1 -0
  223. package/dist/utils/serial-executor.d.ts +5 -0
  224. package/dist/utils/serial-executor.js +12 -0
  225. package/dist/utils/serial-executor.js.map +1 -0
  226. package/package.json +71 -0
  227. package/skills/feique-session/SKILL.md +27 -0
@@ -0,0 +1,3785 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { buildHelpText, isReadOnlyCommand, normalizeIncomingText, parseBridgeCommand, } from './commands.js';
5
+ import { buildConversationKey } from '../state/session-store.js';
6
+ import { buildMessageCard, buildStatusCard } from '../feishu/cards.js';
7
+ import { TaskQueue } from './task-queue.js';
8
+ import { AuditLog } from '../state/audit-log.js';
9
+ import { IdempotencyStore } from '../state/idempotency-store.js';
10
+ import { RunStateStore } from '../state/run-state-store.js';
11
+ import { isProcessAlive, terminateProcess } from '../runtime/process.js';
12
+ import { resolveKnowledgeRoots, searchKnowledgeBase } from '../knowledge/search.js';
13
+ import { FeishuWikiClient } from '../feishu/wiki.js';
14
+ import { FeishuDocClient } from '../feishu/doc.js';
15
+ import { FeishuBaseClient } from '../feishu/base.js';
16
+ import { FeishuTaskClient } from '../feishu/task.js';
17
+ import { resolveMessageResources } from '../feishu/message-resource.js';
18
+ import { MemoryStore } from '../state/memory-store.js';
19
+ import { retrieveMemoryContext } from '../memory/retrieve.js';
20
+ import { summarizeThreadTurn } from '../memory/summarize.js';
21
+ import { CodexSessionIndex } from '../codex/session-index.js';
22
+ import { resolveProjectBackendWithOverride, resolveProjectBackendName } from '../backend/factory.js';
23
+ import { bindProjectAlias, createProjectAlias, removeProjectAlias, updateProjectConfig, updateStringList } from '../config/mutate.js';
24
+ import { buildFeishuPost, truncateForFeishuCard } from '../feishu/text.js';
25
+ import { ConfigHistoryStore } from '../state/config-history-store.js';
26
+ import { loadBridgeConfigFile } from '../config/load.js';
27
+ import { writeUtf8Atomic } from '../utils/fs.js';
28
+ import { expandHomePath } from '../utils/path.js';
29
+ import { canAccessGlobalCapability, canAccessProject, canAccessProjectCapability, describeMinimumRole, filterAccessibleProjects, resolveProjectAccessRole } from '../security/access.js';
30
+ import { adoptProjectSession as adoptSharedProjectSession, listBridgeSessions as listSharedBridgeSessions, switchProjectBinding as switchSharedProjectBinding } from '../control-plane/project-session.js';
31
+ import { getProjectArchiveDir, getProjectAuditDir, getProjectAuditFile, getProjectCacheDir, getProjectDownloadsDir, getProjectTempDir } from '../projects/paths.js';
32
+ import { buildTeamActivityView, detectOverlaps, formatTeamView, formatOverlapAlerts } from '../collaboration/awareness.js';
33
+ import { extractInsights, buildLearnInput, formatRecallResults } from '../collaboration/knowledge.js';
34
+ import { createHandoff, acceptHandoff, createReview, resolveReview, formatHandoff, formatReview, formatReviewResult } from '../collaboration/handoff.js';
35
+ import { analyzeTeamHealth, formatInsightsReport } from '../collaboration/insights.js';
36
+ import { classifyOperation, enforceTrustBoundary, recordRunOutcome, formatTrustState, DEFAULT_TRUST_POLICY } from '../collaboration/trust.js';
37
+ import { buildProjectTimeline, buildOnboardingContext, formatTimeline, isNewActor } from '../collaboration/timeline.js';
38
+ import { HandoffStore } from '../state/handoff-store.js';
39
+ import { TrustStore } from '../state/trust-store.js';
40
+ import { buildTeamDigest, formatTeamDigest, createDigestPeriod } from '../collaboration/digest.js';
41
+ import { estimateCost } from '../observability/cost.js';
42
+ export class FeiqueService {
43
+ config;
44
+ feishuClient;
45
+ sessionStore;
46
+ auditLog;
47
+ logger;
48
+ metrics;
49
+ idempotencyStore;
50
+ runStateStore;
51
+ memoryStore;
52
+ codexSessionIndex;
53
+ runtimeControl;
54
+ adminAuditLog;
55
+ configHistoryStore;
56
+ handoffStore;
57
+ trustStore;
58
+ queue = new TaskQueue();
59
+ projectRootQueue = new TaskQueue();
60
+ activeRuns = new Map();
61
+ runReplyTargets = new Map();
62
+ chatRateWindows = new Map();
63
+ maintenanceTimer;
64
+ digestTimer;
65
+ constructor(config, feishuClient, sessionStore, auditLog, logger, metrics, idempotencyStore = new IdempotencyStore(config.storage.dir), runStateStore = new RunStateStore(config.storage.dir), memoryStore = new MemoryStore(config.storage.dir), codexSessionIndex = new CodexSessionIndex(), runtimeControl, adminAuditLog = new AuditLog(config.storage.dir, 'admin-audit.jsonl'), configHistoryStore = new ConfigHistoryStore(config.storage.dir), handoffStore = new HandoffStore(config.storage.dir), trustStore = new TrustStore(config.storage.dir)) {
66
+ this.config = config;
67
+ this.feishuClient = feishuClient;
68
+ this.sessionStore = sessionStore;
69
+ this.auditLog = auditLog;
70
+ this.logger = logger;
71
+ this.metrics = metrics;
72
+ this.idempotencyStore = idempotencyStore;
73
+ this.runStateStore = runStateStore;
74
+ this.memoryStore = memoryStore;
75
+ this.codexSessionIndex = codexSessionIndex;
76
+ this.runtimeControl = runtimeControl;
77
+ this.adminAuditLog = adminAuditLog;
78
+ this.configHistoryStore = configHistoryStore;
79
+ this.handoffStore = handoffStore;
80
+ this.trustStore = trustStore;
81
+ }
82
+ async recoverRuntimeState() {
83
+ const recovered = await this.runStateStore.recoverOrphanedRuns();
84
+ for (const run of recovered) {
85
+ await this.auditLog.append({
86
+ type: 'codex.run.recovered',
87
+ run_id: run.run_id,
88
+ project_alias: run.project_alias,
89
+ conversation_key: run.conversation_key,
90
+ status: run.status,
91
+ pid: run.pid,
92
+ });
93
+ }
94
+ return recovered;
95
+ }
96
+ startMaintenanceLoop() {
97
+ if (this.maintenanceTimer) {
98
+ return;
99
+ }
100
+ const intervals = [];
101
+ if (this.config.service.memory_enabled) {
102
+ intervals.push(this.config.service.memory_cleanup_interval_seconds * 1000);
103
+ }
104
+ intervals.push(this.config.service.audit_cleanup_interval_seconds * 1000);
105
+ const intervalMs = Math.min(...intervals.filter((value) => Number.isFinite(value) && value > 0));
106
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
107
+ return;
108
+ }
109
+ this.maintenanceTimer = setInterval(() => {
110
+ void this.runMaintenanceCycle();
111
+ }, intervalMs);
112
+ this.maintenanceTimer.unref?.();
113
+ this.startDigestLoop();
114
+ }
115
+ stopMaintenanceLoop() {
116
+ if (this.maintenanceTimer) {
117
+ clearInterval(this.maintenanceTimer);
118
+ this.maintenanceTimer = undefined;
119
+ }
120
+ if (this.digestTimer) {
121
+ clearInterval(this.digestTimer);
122
+ this.digestTimer = undefined;
123
+ }
124
+ }
125
+ startDigestLoop() {
126
+ if (this.digestTimer || !this.config.service.team_digest_enabled) {
127
+ return;
128
+ }
129
+ if (this.config.service.team_digest_chat_ids.length === 0) {
130
+ return;
131
+ }
132
+ const intervalMs = this.config.service.team_digest_interval_hours * 3600_000;
133
+ this.digestTimer = setInterval(() => {
134
+ void this.runDigestCycle();
135
+ }, intervalMs);
136
+ this.digestTimer.unref?.();
137
+ }
138
+ async runDigestCycle() {
139
+ const chatIds = this.config.service.team_digest_chat_ids;
140
+ if (chatIds.length === 0)
141
+ return;
142
+ try {
143
+ const period = createDigestPeriod(this.config.service.team_digest_interval_hours);
144
+ const runs = await this.runStateStore.listRuns();
145
+ const memories = this.config.service.memory_enabled
146
+ ? await this.memoryStore.listRecentMemories({ scope: 'project', project_alias: '' }, 100)
147
+ : [];
148
+ const auditEvents = await this.auditLog.tail(500);
149
+ const digest = buildTeamDigest(runs, memories, auditEvents, period);
150
+ if (digest.summary.total_runs === 0) {
151
+ return; // Nothing to report
152
+ }
153
+ const text = formatTeamDigest(digest);
154
+ for (const chatId of chatIds) {
155
+ try {
156
+ await this.feishuClient.sendText(chatId, text);
157
+ }
158
+ catch (error) {
159
+ this.logger.warn({ chatId, error }, 'Failed to send team digest');
160
+ }
161
+ }
162
+ await this.auditLog.append({
163
+ type: 'collaboration.digest.sent',
164
+ period_label: period.label,
165
+ total_runs: digest.summary.total_runs,
166
+ chat_ids: chatIds,
167
+ });
168
+ // Send per-project mini-digests to project notification chats
169
+ for (const projectDigest of digest.topProjects) {
170
+ const projectChatIds = this.config.projects[projectDigest.alias]?.notification_chat_ids ?? [];
171
+ if (projectChatIds.length === 0)
172
+ continue;
173
+ const successPct = Math.round(projectDigest.success_rate * 100);
174
+ const miniDigestText = [
175
+ `📊 项目摘要 [${projectDigest.alias}] — ${period.label}`,
176
+ `运行: ${projectDigest.runs} | 成功率: ${successPct}%`,
177
+ `参与者: ${projectDigest.actors.join(', ') || '无'}`,
178
+ ].join('\n');
179
+ for (const chatId of projectChatIds) {
180
+ try {
181
+ await this.feishuClient.sendText(chatId, miniDigestText);
182
+ }
183
+ catch { /* best-effort */ }
184
+ }
185
+ }
186
+ }
187
+ catch (error) {
188
+ this.logger.error({ error }, 'Failed to generate team digest');
189
+ }
190
+ }
191
+ async runMemoryMaintenance() {
192
+ if (!this.config.service.memory_enabled) {
193
+ return 0;
194
+ }
195
+ const cleaned = await this.memoryStore.cleanupExpiredMemories();
196
+ if (cleaned > 0) {
197
+ await this.auditLog.append({
198
+ type: 'memory.archive.expired.maintenance',
199
+ count: cleaned,
200
+ });
201
+ this.logger.info({ cleaned }, 'Expired memories cleaned by background maintenance');
202
+ }
203
+ return cleaned;
204
+ }
205
+ async runAuditMaintenance() {
206
+ const auditTargets = this.listManagedAuditTargets();
207
+ let scanned = 0;
208
+ let archived = 0;
209
+ let removed = 0;
210
+ for (const target of auditTargets) {
211
+ const auditLog = new AuditLog(target.stateDir, target.fileName);
212
+ const result = await auditLog.cleanup({
213
+ retentionDays: this.config.service.audit_retention_days,
214
+ archiveAfterDays: this.config.service.audit_archive_after_days,
215
+ archiveDir: target.archiveDir,
216
+ });
217
+ scanned += 1;
218
+ archived += result.archived;
219
+ removed += result.removed;
220
+ }
221
+ if (archived > 0 || removed > 0) {
222
+ await this.auditLog.append({
223
+ type: 'audit.cleanup.completed',
224
+ scanned,
225
+ archived,
226
+ removed,
227
+ });
228
+ this.logger.info({ scanned, archived, removed }, 'Audit retention cleanup completed');
229
+ }
230
+ return { scanned, archived, removed };
231
+ }
232
+ async runMaintenanceCycle() {
233
+ if (this.config.service.memory_enabled) {
234
+ await this.runMemoryMaintenance();
235
+ }
236
+ await this.runAuditMaintenance();
237
+ }
238
+ async handleIncomingMessage(context) {
239
+ if (!context.text.trim() && context.attachments.length === 0) {
240
+ return;
241
+ }
242
+ if (context.sender_type && context.sender_type !== 'user') {
243
+ this.logger.info({ chatId: context.chat_id, senderType: context.sender_type, messageId: context.message_id }, 'Ignoring non-user message');
244
+ return;
245
+ }
246
+ if (context.message_id) {
247
+ const key = buildMessageDedupeKey(context);
248
+ const dedupe = await this.idempotencyStore.register(key, 'message', this.config.service.idempotency_ttl_seconds);
249
+ if (dedupe.duplicate) {
250
+ this.metrics?.recordDuplicateEvent('message');
251
+ await this.auditLog.append({
252
+ type: 'message.duplicate_ignored',
253
+ message_id: context.message_id,
254
+ chat_id: context.chat_id,
255
+ actor_id: context.actor_id,
256
+ });
257
+ return;
258
+ }
259
+ }
260
+ if (!Object.keys(this.config.projects).length) {
261
+ await this.sendTextReply(context.chat_id, '未配置任何项目。请先执行 `feique bind <alias> <path>`。', context.message_id, context.text);
262
+ return;
263
+ }
264
+ const normalizedText = normalizeIncomingText(context.text);
265
+ const selectionKey = await this.getSelectionConversationKey(context);
266
+ const command = parseBridgeCommand(context.text);
267
+ this.metrics?.recordIncomingMessage(context.chat_type, command.kind);
268
+ await this.auditLog.append({
269
+ type: 'message.received',
270
+ chat_id: context.chat_id,
271
+ actor_id: context.actor_id,
272
+ command: command.kind,
273
+ message_id: context.message_id,
274
+ text: context.text,
275
+ message_type: context.message_type,
276
+ attachment_count: context.attachments.length,
277
+ });
278
+ try {
279
+ switch (command.kind) {
280
+ case 'help':
281
+ await this.sendTextReply(context.chat_id, buildHelpText(), context.message_id, context.text);
282
+ return;
283
+ case 'projects':
284
+ await this.sendTextReply(context.chat_id, await this.buildProjectsText(selectionKey, context.chat_id), context.message_id, context.text);
285
+ return;
286
+ case 'project':
287
+ await this.handleProjectCommand(context, selectionKey, command.alias, command.followupPrompt);
288
+ return;
289
+ case 'status':
290
+ await this.handleStatusCommand(context, selectionKey, command.detail === true);
291
+ return;
292
+ case 'new':
293
+ await this.handleNewCommand(context, selectionKey);
294
+ return;
295
+ case 'cancel':
296
+ await this.handleCancelCommand(context, selectionKey);
297
+ return;
298
+ case 'kb':
299
+ await this.handleKnowledgeCommand(context, selectionKey, command.action, command.query);
300
+ return;
301
+ case 'doc':
302
+ await this.handleDocCommand(context, selectionKey, command.action, command.value, command.extra);
303
+ return;
304
+ case 'task':
305
+ await this.handleTaskCommand(context, selectionKey, command.action, command.value);
306
+ return;
307
+ case 'base':
308
+ await this.handleBaseCommand(context, selectionKey, command.action, command.appToken, command.tableId, command.recordId, command.value);
309
+ return;
310
+ case 'memory':
311
+ await this.handleMemoryCommand(context, selectionKey, command.action, command.scope, command.value, command.filters);
312
+ return;
313
+ case 'wiki':
314
+ await this.handleWikiCommand(context, selectionKey, command.action, command.value, command.extra, command.target, command.role);
315
+ return;
316
+ case 'backend':
317
+ await this.handleBackendCommand(context, selectionKey, command.name);
318
+ return;
319
+ case 'session':
320
+ const sessionArgument = command.action === 'adopt' ? command.target : command.threadId;
321
+ await this.handleSessionCommand(context, selectionKey, command.action, sessionArgument);
322
+ return;
323
+ case 'admin':
324
+ await this.handleAdminCommand(context, selectionKey, command);
325
+ return;
326
+ case 'team':
327
+ await this.handleTeamCommand(context);
328
+ return;
329
+ case 'learn':
330
+ this.metrics?.recordCollaborationEvent('learn');
331
+ await this.handleLearnCommand(context, selectionKey, command.value);
332
+ return;
333
+ case 'recall':
334
+ this.metrics?.recordCollaborationEvent('recall');
335
+ await this.handleRecallCommand(context, selectionKey, command.query);
336
+ return;
337
+ case 'handoff':
338
+ this.metrics?.recordCollaborationEvent('handoff');
339
+ await this.handleHandoffCommand(context, selectionKey, command.summary);
340
+ return;
341
+ case 'pickup':
342
+ this.metrics?.recordCollaborationEvent('pickup');
343
+ await this.handlePickupCommand(context, selectionKey, command.id);
344
+ return;
345
+ case 'review':
346
+ this.metrics?.recordCollaborationEvent('review');
347
+ await this.handleReviewCommand(context, selectionKey);
348
+ return;
349
+ case 'approve':
350
+ this.metrics?.recordCollaborationEvent('approve');
351
+ await this.handleApproveCommand(context, command.comment);
352
+ return;
353
+ case 'reject':
354
+ this.metrics?.recordCollaborationEvent('reject');
355
+ await this.handleRejectCommand(context, command.reason);
356
+ return;
357
+ case 'insights':
358
+ await this.handleInsightsCommand(context);
359
+ return;
360
+ case 'trust':
361
+ await this.handleTrustCommand(context, selectionKey, command.action, command.level);
362
+ return;
363
+ case 'timeline':
364
+ await this.handleTimelineCommand(context, selectionKey, command.project);
365
+ return;
366
+ case 'digest':
367
+ this.metrics?.recordCollaborationEvent('digest');
368
+ await this.handleDigestCommand(context);
369
+ return;
370
+ case 'prompt':
371
+ await this.handlePromptMessage(context, selectionKey, command.prompt, context.text);
372
+ return;
373
+ }
374
+ }
375
+ catch (error) {
376
+ const message = error instanceof Error ? error.message : String(error);
377
+ this.logger.error({ error, chatId: context.chat_id, actorId: context.actor_id, command: command.kind }, 'Failed to handle incoming message');
378
+ await this.auditLog.append({
379
+ type: 'message.failed',
380
+ chat_id: context.chat_id,
381
+ actor_id: context.actor_id,
382
+ command: command.kind,
383
+ error: message,
384
+ message_id: context.message_id,
385
+ });
386
+ await this.sendTextReply(context.chat_id, `处理失败:\n${message}`, context.message_id, context.text);
387
+ }
388
+ }
389
+ async handleCardAction(context) {
390
+ const action = typeof context.action_value.action === 'string' ? context.action_value.action : 'status';
391
+ const dedupeKey = buildCardDedupeKey(context, action);
392
+ if (dedupeKey) {
393
+ const dedupe = await this.idempotencyStore.register(dedupeKey, 'card', this.config.service.idempotency_ttl_seconds);
394
+ if (dedupe.duplicate) {
395
+ this.metrics?.recordDuplicateEvent('card');
396
+ return buildStatusCard({
397
+ title: '重复操作已忽略',
398
+ summary: '这次卡片动作已经处理过,不会再次提交。',
399
+ projectAlias: typeof context.action_value.project_alias === 'string' ? context.action_value.project_alias : 'unknown',
400
+ includeActions: false,
401
+ });
402
+ }
403
+ }
404
+ this.metrics?.recordCardAction(action);
405
+ await this.auditLog.append({
406
+ type: 'card.action',
407
+ action,
408
+ chat_id: context.chat_id,
409
+ actor_id: context.actor_id,
410
+ open_message_id: context.open_message_id,
411
+ });
412
+ const projectAlias = typeof context.action_value.project_alias === 'string' ? context.action_value.project_alias : undefined;
413
+ const sessionKey = typeof context.action_value.conversation_key === 'string' ? context.action_value.conversation_key : undefined;
414
+ const chatId = typeof context.action_value.chat_id === 'string' ? context.action_value.chat_id : context.chat_id;
415
+ if (!projectAlias || !sessionKey || !chatId) {
416
+ return buildStatusCard({
417
+ title: '无法处理卡片操作',
418
+ summary: '卡片中缺少会话元数据。请直接在飞书里发送文本继续。',
419
+ projectAlias: projectAlias ?? 'unknown',
420
+ includeActions: false,
421
+ });
422
+ }
423
+ const project = this.requireProject(projectAlias);
424
+ const queueKey = buildQueueKey(sessionKey, projectAlias);
425
+ const conversation = await this.sessionStore.getConversation(sessionKey);
426
+ if (!conversation) {
427
+ return buildStatusCard({
428
+ title: '会话不存在',
429
+ summary: '对应的会话状态已经丢失。请发送 `/new` 后重新开始。',
430
+ projectAlias,
431
+ includeActions: false,
432
+ });
433
+ }
434
+ if (action === 'new') {
435
+ await this.sessionStore.clearActiveProjectSession(sessionKey, projectAlias);
436
+ return buildStatusCard({
437
+ title: '会话已重置',
438
+ summary: '下一条文本消息会启动一个新的 Codex 会话。',
439
+ projectAlias,
440
+ includeActions: false,
441
+ });
442
+ }
443
+ if (action === 'cancel') {
444
+ const cancelled = await this.cancelActiveRun(queueKey, 'user');
445
+ return buildStatusCard({
446
+ title: cancelled ? '已提交取消' : '没有可取消的运行',
447
+ summary: cancelled ? '当前项目的运行正在停止。' : '当前项目没有活动中的运行。',
448
+ projectAlias,
449
+ includeActions: false,
450
+ });
451
+ }
452
+ if (action === 'rerun') {
453
+ const previousPrompt = conversation.projects[projectAlias]?.last_prompt;
454
+ if (!previousPrompt) {
455
+ return buildStatusCard({
456
+ title: '无法重试',
457
+ summary: '没有找到上一轮提示词,请直接发新消息。',
458
+ projectAlias,
459
+ includeActions: false,
460
+ });
461
+ }
462
+ const scheduled = await this.scheduleProjectExecution({
463
+ projectAlias,
464
+ project,
465
+ sessionKey,
466
+ queueKey,
467
+ }, {
468
+ chatId,
469
+ actorId: context.actor_id,
470
+ prompt: previousPrompt,
471
+ }, async (runId) => {
472
+ await this.executePrompt({
473
+ runId,
474
+ chatId,
475
+ actorId: context.actor_id,
476
+ tenantKey: context.tenant_key,
477
+ projectAlias,
478
+ project,
479
+ incomingMessage: {
480
+ tenant_key: context.tenant_key,
481
+ chat_id: chatId,
482
+ chat_type: 'unknown',
483
+ actor_id: context.actor_id,
484
+ message_id: context.open_message_id ?? `card-rerun-${Date.now()}`,
485
+ message_type: 'card-action',
486
+ text: previousPrompt,
487
+ attachments: [],
488
+ mentions: [],
489
+ raw: context.raw,
490
+ },
491
+ prompt: previousPrompt,
492
+ sessionKey,
493
+ queueKey,
494
+ });
495
+ });
496
+ scheduled.release();
497
+ void scheduled.completion.catch((error) => {
498
+ this.logger.error({ error, projectAlias }, 'Queued rerun execution failed unexpectedly');
499
+ });
500
+ return buildStatusCard({
501
+ title: scheduled.queued ? '已加入排队' : '已提交重试',
502
+ summary: scheduled.queued?.detail ?? '桥接器正在重新执行上一轮,结果会通过消息回传。',
503
+ projectAlias,
504
+ sessionId: conversation.projects[projectAlias]?.thread_id,
505
+ runStatus: scheduled.queued ? 'queued' : undefined,
506
+ runPhase: scheduled.queued ? '排队中' : undefined,
507
+ includeActions: false,
508
+ });
509
+ }
510
+ return this.buildStatusCardFromConversation(projectAlias, sessionKey, conversation, await this.runStateStore.getLatestVisibleRun(queueKey));
511
+ }
512
+ async listRuns() {
513
+ return this.runStateStore.listRuns();
514
+ }
515
+ async executePrompt(input) {
516
+ const conversation = (await this.sessionStore.getConversation(input.sessionKey)) ??
517
+ (await this.sessionStore.ensureConversation(input.sessionKey, {
518
+ chat_id: input.chatId,
519
+ actor_id: input.actorId,
520
+ tenant_key: input.tenantKey,
521
+ scope: input.project.session_scope,
522
+ }));
523
+ const currentSession = conversation.projects[input.projectAlias];
524
+ if (this.config.service.memory_enabled) {
525
+ await this.memoryStore.cleanupExpiredMemories();
526
+ }
527
+ const memoryContext = this.config.service.memory_enabled
528
+ ? await retrieveMemoryContext(this.memoryStore, {
529
+ conversationKey: input.sessionKey,
530
+ projectAlias: input.projectAlias,
531
+ threadId: currentSession?.thread_id,
532
+ query: input.prompt,
533
+ searchLimit: this.config.service.memory_search_limit,
534
+ groupChatId: input.incomingMessage.chat_type === 'group' ? input.incomingMessage.chat_id : undefined,
535
+ includeGroupMemories: this.config.service.memory_group_enabled && input.incomingMessage.chat_type === 'group',
536
+ })
537
+ : { pinnedMemories: [], relevantMemories: [], pinnedGroupMemories: [], relevantGroupMemories: [] };
538
+ // Direction 6: Inject onboarding context for new actors
539
+ let onboardingPrefix = '';
540
+ if (input.actorId && this.config.service.memory_enabled) {
541
+ try {
542
+ const allRuns = await this.runStateStore.listRuns();
543
+ if (isNewActor(input.actorId, allRuns, input.projectAlias)) {
544
+ const memories = await this.memoryStore.listRecentMemories({ scope: 'project', project_alias: input.projectAlias }, 10);
545
+ const timeline = buildProjectTimeline(allRuns, memories, [], input.projectAlias, 10);
546
+ onboardingPrefix = buildOnboardingContext(timeline, memories, input.projectAlias);
547
+ }
548
+ }
549
+ catch { /* onboarding injection is best-effort */ }
550
+ }
551
+ const effectivePrompt = onboardingPrefix
552
+ ? `${onboardingPrefix}\n\n${input.prompt}`
553
+ : input.prompt;
554
+ const bridgePrompt = await this.buildBridgePrompt(input.projectAlias, input.project, input.incomingMessage, effectivePrompt, memoryContext);
555
+ const startedAt = Date.now();
556
+ const projectRoot = this.resolveProjectRoot(input.project);
557
+ const runId = input.runId ?? randomUUID();
558
+ let lastProgressUpdate = 0;
559
+ const activeRun = {
560
+ runId,
561
+ controller: new AbortController(),
562
+ };
563
+ this.activeRuns.set(input.queueKey, activeRun);
564
+ await this.updateRunStartedReply(input.chatId, input.projectAlias, runId);
565
+ await this.runStateStore.upsertRun(runId, {
566
+ queue_key: input.queueKey,
567
+ conversation_key: input.sessionKey,
568
+ project_alias: input.projectAlias,
569
+ chat_id: input.chatId,
570
+ actor_id: input.actorId,
571
+ session_id: currentSession?.thread_id,
572
+ project_root: projectRoot,
573
+ prompt_excerpt: truncateExcerpt(input.prompt),
574
+ status: 'running',
575
+ status_detail: undefined,
576
+ });
577
+ await this.auditLog.append({
578
+ type: 'codex.run.started',
579
+ run_id: runId,
580
+ chat_id: input.chatId,
581
+ actor_id: input.actorId,
582
+ project_alias: input.projectAlias,
583
+ conversation_key: input.sessionKey,
584
+ session_id: currentSession?.thread_id,
585
+ prompt: input.prompt,
586
+ });
587
+ await this.appendProjectAuditEvent(input.projectAlias, input.project, {
588
+ type: 'codex.run.started',
589
+ run_id: runId,
590
+ chat_id: input.chatId,
591
+ actor_id: input.actorId,
592
+ session_id: currentSession?.thread_id,
593
+ project_root: projectRoot,
594
+ });
595
+ this.logger.info({
596
+ runId,
597
+ queueKey: input.queueKey,
598
+ sessionKey: input.sessionKey,
599
+ projectAlias: input.projectAlias,
600
+ projectRoot,
601
+ sessionId: currentSession?.thread_id,
602
+ }, 'Codex run started');
603
+ this.metrics?.recordCodexTurnStarted(input.projectAlias, runId);
604
+ try {
605
+ const sessionBackendOverride = await this.sessionStore.getProjectBackend(input.sessionKey, input.projectAlias);
606
+ const backend = this.resolveBackendByName(input.projectAlias, sessionBackendOverride);
607
+ const backendLabel = backend.name === 'claude' ? 'Claude' : 'Codex';
608
+ const outputTokenLimit = backend.name === 'claude'
609
+ ? (this.config.claude?.output_token_limit ?? this.config.codex.output_token_limit)
610
+ : this.config.codex.output_token_limit;
611
+ const result = await backend.run({
612
+ workdir: input.project.root,
613
+ prompt: bridgePrompt,
614
+ sessionId: currentSession?.thread_id,
615
+ timeoutMs: backend.name === 'claude'
616
+ ? (this.config.claude?.run_timeout_ms ?? this.config.codex.run_timeout_ms)
617
+ : this.config.codex.run_timeout_ms,
618
+ signal: activeRun.controller.signal,
619
+ logger: this.logger,
620
+ projectConfig: backend.name === 'codex'
621
+ ? {
622
+ profile: input.project.profile ?? this.config.codex.default_profile,
623
+ sandbox: input.project.sandbox ?? this.config.codex.default_sandbox,
624
+ tempDir: this.resolveProjectTempDir(input.projectAlias, input.project),
625
+ cacheDir: this.resolveProjectCacheDir(input.projectAlias, input.project),
626
+ }
627
+ : {
628
+ permissionMode: input.project.claude_permission_mode ?? this.config.claude?.default_permission_mode,
629
+ model: input.project.claude_model ?? this.config.claude?.default_model,
630
+ maxBudgetUsd: input.project.claude_max_budget_usd ?? this.config.claude?.max_budget_usd,
631
+ allowedTools: input.project.claude_allowed_tools ?? this.config.claude?.allowed_tools,
632
+ systemPromptAppend: input.project.claude_system_prompt_append ?? this.config.claude?.system_prompt_append,
633
+ },
634
+ onSpawn: async (pid) => {
635
+ activeRun.pid = pid;
636
+ await this.runStateStore.upsertRun(runId, {
637
+ queue_key: input.queueKey,
638
+ conversation_key: input.sessionKey,
639
+ project_alias: input.projectAlias,
640
+ chat_id: input.chatId,
641
+ actor_id: input.actorId,
642
+ session_id: currentSession?.thread_id,
643
+ project_root: projectRoot,
644
+ prompt_excerpt: truncateExcerpt(input.prompt),
645
+ status: 'running',
646
+ status_detail: undefined,
647
+ pid,
648
+ });
649
+ },
650
+ onEvent: async (event) => {
651
+ if (!this.config.service.emit_progress_updates) {
652
+ return;
653
+ }
654
+ const message = backend.summarizeEvent(event);
655
+ if (!message) {
656
+ return;
657
+ }
658
+ const now = Date.now();
659
+ if (now - lastProgressUpdate < this.config.service.progress_update_interval_ms) {
660
+ return;
661
+ }
662
+ lastProgressUpdate = now;
663
+ await this.updateRunProgressReply(input, runId, message);
664
+ },
665
+ });
666
+ const excerpt = result.finalMessage.slice(0, outputTokenLimit);
667
+ if (!excerpt.trim()) {
668
+ this.logger.warn({
669
+ runId,
670
+ queueKey: input.queueKey,
671
+ sessionKey: input.sessionKey,
672
+ projectAlias: input.projectAlias,
673
+ sessionId: result.sessionId,
674
+ durationMs: Date.now() - startedAt,
675
+ }, 'Codex run completed without a displayable final message');
676
+ }
677
+ const cardSummary = truncateForFeishuCard(excerpt || `${backendLabel} 已完成,但没有返回可显示文本。`);
678
+ await this.auditLog.append({
679
+ type: 'codex.run.completed',
680
+ run_id: runId,
681
+ chat_id: input.chatId,
682
+ actor_id: input.actorId,
683
+ project_alias: input.projectAlias,
684
+ conversation_key: input.sessionKey,
685
+ session_id: result.sessionId,
686
+ exit_code: result.exitCode,
687
+ duration_ms: Date.now() - startedAt,
688
+ backend: backend.name,
689
+ });
690
+ await this.appendProjectAuditEvent(input.projectAlias, input.project, {
691
+ type: 'codex.run.completed',
692
+ run_id: runId,
693
+ chat_id: input.chatId,
694
+ actor_id: input.actorId,
695
+ session_id: result.sessionId,
696
+ duration_ms: Date.now() - startedAt,
697
+ backend: backend.name,
698
+ });
699
+ this.logger.info({
700
+ runId,
701
+ queueKey: input.queueKey,
702
+ sessionKey: input.sessionKey,
703
+ projectAlias: input.projectAlias,
704
+ sessionId: result.sessionId,
705
+ exitCode: result.exitCode,
706
+ finalMessageChars: excerpt.length,
707
+ durationMs: Date.now() - startedAt,
708
+ }, 'Codex run completed');
709
+ await this.sessionStore.upsertProjectSession(input.sessionKey, input.projectAlias, {
710
+ thread_id: result.sessionId,
711
+ last_prompt: input.prompt,
712
+ last_response_excerpt: excerpt,
713
+ });
714
+ if (this.config.service.memory_enabled && result.sessionId) {
715
+ const summaryDraft = summarizeThreadTurn({
716
+ previousSummary: memoryContext.threadSummary?.summary,
717
+ prompt: input.prompt,
718
+ responseExcerpt: excerpt,
719
+ maxChars: this.config.service.thread_summary_max_chars,
720
+ });
721
+ const threadSummary = await this.memoryStore.upsertThreadSummary({
722
+ conversation_key: input.sessionKey,
723
+ project_alias: input.projectAlias,
724
+ thread_id: result.sessionId,
725
+ summary: summaryDraft.summary,
726
+ recent_prompt: input.prompt,
727
+ recent_response_excerpt: excerpt,
728
+ files_touched: summaryDraft.filesTouched,
729
+ open_tasks: summaryDraft.openTasks,
730
+ decisions: summaryDraft.decisions,
731
+ });
732
+ await this.auditLog.append({
733
+ type: 'memory.thread_summary.updated',
734
+ run_id: runId,
735
+ project_alias: input.projectAlias,
736
+ conversation_key: input.sessionKey,
737
+ thread_id: result.sessionId,
738
+ files_touched: threadSummary.files_touched,
739
+ });
740
+ }
741
+ await this.enforceSessionHistoryLimit(input.sessionKey, input.projectAlias);
742
+ await this.runStateStore.upsertRun(runId, {
743
+ queue_key: input.queueKey,
744
+ conversation_key: input.sessionKey,
745
+ project_alias: input.projectAlias,
746
+ chat_id: input.chatId,
747
+ actor_id: input.actorId,
748
+ session_id: result.sessionId,
749
+ project_root: projectRoot,
750
+ pid: activeRun.pid,
751
+ prompt_excerpt: truncateExcerpt(input.prompt),
752
+ status: 'success',
753
+ status_detail: undefined,
754
+ input_tokens: result.inputTokens,
755
+ output_tokens: result.outputTokens,
756
+ estimated_cost_usd: estimateCost(result.inputTokens, result.outputTokens, backend.name),
757
+ });
758
+ this.metrics?.recordCodexTurn('success', input.projectAlias, (Date.now() - startedAt) / 1000, runId);
759
+ // Record cost and token metrics
760
+ if (result.inputTokens || result.outputTokens) {
761
+ const costUsd = estimateCost(result.inputTokens, result.outputTokens, backend.name) ?? 0;
762
+ this.metrics?.recordCost(input.projectAlias, backend.name, costUsd);
763
+ this.metrics?.recordTokens(input.projectAlias, backend.name, result.inputTokens ?? 0, result.outputTokens ?? 0);
764
+ }
765
+ // Direction 5: Record trust outcome
766
+ try {
767
+ const trustState = await this.trustStore.getOrCreate(input.projectAlias);
768
+ const updated = recordRunOutcome(trustState, true, DEFAULT_TRUST_POLICY);
769
+ await this.trustStore.update(input.projectAlias, updated);
770
+ this.metrics?.recordTrustLevel(input.projectAlias, updated.current_level);
771
+ }
772
+ catch { /* trust tracking is best-effort */ }
773
+ // Direction 2: Auto-extract knowledge
774
+ if (this.config.service.memory_enabled && excerpt.length >= 100) {
775
+ try {
776
+ const insight = extractInsights(input.prompt, excerpt, input.projectAlias);
777
+ if (insight) {
778
+ await this.memoryStore.saveProjectMemory({
779
+ project_alias: insight.project_alias,
780
+ title: insight.title,
781
+ content: insight.content,
782
+ tags: insight.tags,
783
+ source: 'auto',
784
+ created_by: input.actorId,
785
+ });
786
+ }
787
+ }
788
+ catch { /* auto-extraction is best-effort */ }
789
+ }
790
+ await this.sendOrUpdateRunOutcome({
791
+ input,
792
+ runId,
793
+ title: `${backendLabel} 已完成`,
794
+ body: excerpt || `${backendLabel} 已完成,但没有返回可显示文本。`,
795
+ runStatus: 'success',
796
+ runPhase: '已完成',
797
+ cardSummary,
798
+ sessionId: result.sessionId,
799
+ });
800
+ }
801
+ catch (error) {
802
+ const message = error instanceof Error ? error.message : String(error);
803
+ const cancelled = error instanceof Error && error.name === 'AbortError' && activeRun.cancelReason === 'user';
804
+ const status = cancelled ? 'cancelled' : 'failure';
805
+ if (!cancelled && error instanceof Error && error.name === 'AbortError') {
806
+ activeRun.cancelReason = 'timeout';
807
+ }
808
+ if (!cancelled && activeRun.cancelReason === 'timeout') {
809
+ this.metrics?.recordCodexTurn('failure', input.projectAlias, (Date.now() - startedAt) / 1000, runId);
810
+ }
811
+ else {
812
+ this.metrics?.recordCodexTurn(cancelled ? 'cancelled' : 'failure', input.projectAlias, (Date.now() - startedAt) / 1000, runId);
813
+ }
814
+ await this.runStateStore.upsertRun(runId, {
815
+ queue_key: input.queueKey,
816
+ conversation_key: input.sessionKey,
817
+ project_alias: input.projectAlias,
818
+ chat_id: input.chatId,
819
+ actor_id: input.actorId,
820
+ session_id: currentSession?.thread_id,
821
+ project_root: projectRoot,
822
+ pid: activeRun.pid,
823
+ prompt_excerpt: truncateExcerpt(input.prompt),
824
+ status,
825
+ status_detail: undefined,
826
+ error: message,
827
+ });
828
+ await this.auditLog.append({
829
+ type: cancelled ? 'codex.run.cancelled' : 'codex.run.failed',
830
+ run_id: runId,
831
+ chat_id: input.chatId,
832
+ actor_id: input.actorId,
833
+ project_alias: input.projectAlias,
834
+ conversation_key: input.sessionKey,
835
+ error: message,
836
+ });
837
+ await this.appendProjectAuditEvent(input.projectAlias, input.project, {
838
+ type: cancelled ? 'codex.run.cancelled' : 'codex.run.failed',
839
+ run_id: runId,
840
+ chat_id: input.chatId,
841
+ actor_id: input.actorId,
842
+ error: message,
843
+ });
844
+ // Direction 5: Record trust failure (only for actual failures, not cancellations)
845
+ if (!cancelled) {
846
+ try {
847
+ const trustState = await this.trustStore.getOrCreate(input.projectAlias);
848
+ const updated = recordRunOutcome(trustState, false, DEFAULT_TRUST_POLICY);
849
+ await this.trustStore.update(input.projectAlias, updated);
850
+ }
851
+ catch { /* trust tracking is best-effort */ }
852
+ // Notify project chats about the failure
853
+ await this.notifyProjectChats(input.projectAlias, `❌ 运行失败 [${input.projectAlias}]\n${message.slice(0, 200)}`);
854
+ }
855
+ if (cancelled) {
856
+ this.logger.warn({
857
+ runId,
858
+ queueKey: input.queueKey,
859
+ sessionKey: input.sessionKey,
860
+ projectAlias: input.projectAlias,
861
+ durationMs: Date.now() - startedAt,
862
+ }, 'Codex run cancelled');
863
+ }
864
+ else {
865
+ this.logger.error({
866
+ error,
867
+ runId,
868
+ queueKey: input.queueKey,
869
+ sessionKey: input.sessionKey,
870
+ projectAlias: input.projectAlias,
871
+ durationMs: Date.now() - startedAt,
872
+ }, 'Codex run failed');
873
+ }
874
+ await this.sendOrUpdateRunOutcome({
875
+ input,
876
+ runId,
877
+ title: cancelled ? '运行已取消' : '执行失败',
878
+ body: cancelled ? '当前运行已取消。' : ['执行失败。', '', message].join('\n'),
879
+ runStatus: cancelled ? 'cancelled' : 'failure',
880
+ runPhase: cancelled ? '已取消' : '失败',
881
+ cardSummary: truncateForFeishuCard(cancelled ? '当前运行已取消。' : message),
882
+ });
883
+ }
884
+ finally {
885
+ this.activeRuns.delete(input.queueKey);
886
+ this.runReplyTargets.delete(runId);
887
+ }
888
+ }
889
+ async handleProjectCommand(context, selectionKey, alias, followupPrompt) {
890
+ if (!alias) {
891
+ const currentAlias = await this.resolveProjectAlias(selectionKey);
892
+ if (!canAccessProject(this.config, currentAlias, context.chat_id, 'viewer')) {
893
+ await this.sendTextReply(context.chat_id, `当前 chat_id 无权查看项目 ${currentAlias}。至少需要 ${describeMinimumRole('viewer')} 权限。`, context.message_id, context.text);
894
+ return;
895
+ }
896
+ const project = this.requireProject(currentAlias);
897
+ await this.sendTextReply(context.chat_id, `当前项目: ${currentAlias}${project.description ? `\n说明: ${project.description}` : ''}`, context.message_id, context.text);
898
+ return;
899
+ }
900
+ if (!canAccessProject(this.config, alias, context.chat_id, 'viewer')) {
901
+ await this.sendTextReply(context.chat_id, `当前 chat_id 无权切换到项目 ${alias}。至少需要 ${describeMinimumRole('viewer')} 权限。`, context.message_id, context.text);
902
+ return;
903
+ }
904
+ const project = this.requireProject(alias);
905
+ const switched = await switchSharedProjectBinding(this.config, this.sessionStore, this.codexSessionIndex, {
906
+ chatId: context.chat_id,
907
+ actorId: context.actor_id,
908
+ tenantKey: context.tenant_key,
909
+ }, alias);
910
+ await this.auditLog.append({
911
+ type: 'project.selected',
912
+ chat_id: context.chat_id,
913
+ actor_id: context.actor_id,
914
+ project_alias: alias,
915
+ });
916
+ if (switched.structured.autoAdoption.kind === 'adopted') {
917
+ await this.auditLog.append({
918
+ type: 'session.adopted',
919
+ project_alias: alias,
920
+ conversation_key: switched.structured.sessionKey,
921
+ thread_id: switched.structured.autoAdoption.session.sessionId,
922
+ source_cwd: switched.structured.autoAdoption.session.cwd,
923
+ source: switched.structured.autoAdoption.session.source,
924
+ match_kind: switched.structured.autoAdoption.session.matchKind,
925
+ backend: switched.structured.autoAdoption.session.backend,
926
+ trigger: 'project-switch',
927
+ });
928
+ }
929
+ if (followupPrompt) {
930
+ const followupCommand = parseBridgeCommand(followupPrompt);
931
+ if (isReadOnlyCommand(followupCommand)) {
932
+ await this.handleReadOnlyFollowupCommand(context, selectionKey, followupCommand, followupPrompt);
933
+ return;
934
+ }
935
+ await this.handlePromptMessage(context, selectionKey, followupPrompt, context.text);
936
+ return;
937
+ }
938
+ await this.sendTextReply(context.chat_id, switched.text, context.message_id, context.text);
939
+ }
940
+ async handleReadOnlyFollowupCommand(context, selectionKey, command, followupPrompt) {
941
+ switch (command.kind) {
942
+ case 'help':
943
+ await this.sendTextReply(context.chat_id, buildHelpText(), context.message_id, context.text);
944
+ return;
945
+ case 'projects':
946
+ await this.sendTextReply(context.chat_id, await this.buildProjectsText(selectionKey, context.chat_id), context.message_id, context.text);
947
+ return;
948
+ case 'project':
949
+ await this.handleProjectCommand(context, selectionKey, command.alias);
950
+ return;
951
+ case 'status':
952
+ await this.handleStatusCommand(context, selectionKey, command.detail === true);
953
+ return;
954
+ case 'kb':
955
+ await this.handleKnowledgeCommand(context, selectionKey, command.action, command.query);
956
+ return;
957
+ case 'doc':
958
+ await this.handleDocCommand(context, selectionKey, command.action, command.value, command.extra);
959
+ return;
960
+ case 'task':
961
+ await this.handleTaskCommand(context, selectionKey, command.action, command.value);
962
+ return;
963
+ case 'base':
964
+ await this.handleBaseCommand(context, selectionKey, command.action, command.appToken, command.tableId, command.recordId, command.value);
965
+ return;
966
+ case 'memory':
967
+ await this.handleMemoryCommand(context, selectionKey, command.action, command.scope, command.value, command.filters);
968
+ return;
969
+ case 'wiki':
970
+ await this.handleWikiCommand(context, selectionKey, command.action, command.value, command.extra, command.target, command.role);
971
+ return;
972
+ case 'backend':
973
+ await this.handleBackendCommand(context, selectionKey, command.name);
974
+ return;
975
+ case 'session': {
976
+ const sessionArgument = command.action === 'adopt' ? command.target : command.threadId;
977
+ await this.handleSessionCommand(context, selectionKey, command.action, sessionArgument);
978
+ return;
979
+ }
980
+ case 'admin':
981
+ await this.handleAdminCommand(context, selectionKey, command);
982
+ return;
983
+ case 'prompt':
984
+ await this.handlePromptMessage(context, selectionKey, followupPrompt, context.text);
985
+ return;
986
+ default:
987
+ await this.handlePromptMessage(context, selectionKey, followupPrompt, context.text);
988
+ }
989
+ }
990
+ async handlePromptMessage(context, selectionKey, rawPrompt, originalText) {
991
+ const prompt = normalizeIncomingText(rawPrompt) || (context.attachments.length > 0 ? '请结合这条飞书消息附带的多媒体信息继续处理。' : '');
992
+ if (!prompt) {
993
+ return;
994
+ }
995
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
996
+ if (!this.canExecuteProjectRuns(context.chat_id, projectContext.projectAlias)) {
997
+ await this.sendTextReply(context.chat_id, `当前 chat_id 只有 ${resolveProjectAccessRole(this.config, projectContext.projectAlias, context.chat_id) ?? '未授权'} 权限,执行运行至少需要 ${describeMinimumRole('operator')} 权限。`, context.message_id, context.text);
998
+ return;
999
+ }
1000
+ const resolvedContext = await resolveMessageResources(this.feishuClient.createSdkClient?.(), this.resolveProjectDownloadDir(projectContext.projectAlias, projectContext.project), context, {
1001
+ downloadEnabled: this.config.service.download_message_resources,
1002
+ transcribeAudio: this.config.service.transcribe_audio_messages,
1003
+ transcribeCliPath: this.config.service.transcribe_cli_path,
1004
+ describeImages: this.config.service.describe_image_messages,
1005
+ openaiImageModel: this.config.service.openai_image_model,
1006
+ logger: this.logger,
1007
+ });
1008
+ if (context.chat_type === 'group' && this.shouldRequireMention(projectContext.project) && context.mentions.length === 0) {
1009
+ return;
1010
+ }
1011
+ const rateLimitMessage = this.checkAndConsumeChatRateLimit(projectContext.projectAlias, projectContext.project, context.chat_id);
1012
+ if (rateLimitMessage) {
1013
+ await this.sendTextReply(context.chat_id, rateLimitMessage, context.message_id, context.text);
1014
+ return;
1015
+ }
1016
+ await this.sessionStore.selectProject(selectionKey, projectContext.projectAlias);
1017
+ // Direction 1: Overlap detection — notify when another team member is on the same project
1018
+ try {
1019
+ const activeRuns = await this.runStateStore.listRuns();
1020
+ const overlaps = detectOverlaps({ actor_id: context.actor_id, project_alias: projectContext.projectAlias, project_root: projectContext.project.root }, activeRuns);
1021
+ if (overlaps.length > 0) {
1022
+ const alertText = formatOverlapAlerts(overlaps);
1023
+ await this.sendTextReply(context.chat_id, alertText, context.message_id, context.text);
1024
+ }
1025
+ }
1026
+ catch { /* overlap detection is best-effort */ }
1027
+ // Direction 5: Trust boundary check
1028
+ try {
1029
+ const trustState = await this.trustStore.getOrCreate(projectContext.projectAlias);
1030
+ const operationClass = classifyOperation(prompt);
1031
+ const decision = enforceTrustBoundary(trustState.current_level, operationClass);
1032
+ if (!decision.allowed) {
1033
+ await this.sendTextReply(context.chat_id, `🛡️ 信任边界拦截: ${decision.reason ?? '操作不被允许'}`, context.message_id, context.text);
1034
+ return;
1035
+ }
1036
+ if (decision.requires_approval) {
1037
+ // Create an approval request
1038
+ const review = createReview({
1039
+ run_id: `approval-${Date.now()}`,
1040
+ project_alias: projectContext.projectAlias,
1041
+ chat_id: context.chat_id,
1042
+ actor_id: context.actor_id ?? 'unknown',
1043
+ content_excerpt: `[${operationClass}] ${prompt.slice(0, 100)}`,
1044
+ });
1045
+ await this.handoffStore.addReview(review);
1046
+ // Notify admin chats
1047
+ const adminChatIds = projectContext.project.admin_chat_ids ?? [];
1048
+ const approvalText = `🛡️ 审批请求 [${projectContext.projectAlias}]\n发起人: ${context.actor_id}\n操作类型: ${operationClass}\n内容: "${prompt.slice(0, 80)}"\n\n使用 /approve 批准此操作`;
1049
+ for (const adminChat of adminChatIds) {
1050
+ try {
1051
+ await this.feishuClient.sendText(adminChat, approvalText);
1052
+ }
1053
+ catch { /* best-effort */ }
1054
+ }
1055
+ // Notify project notification chats
1056
+ await this.notifyProjectChats(projectContext.projectAlias, approvalText);
1057
+ // Tell the user their request is pending
1058
+ await this.sendTextReply(context.chat_id, `⏳ ${decision.reason ?? '此操作需要审批'}\n已通知管理员,等待审批中...`, context.message_id, context.text);
1059
+ await this.auditLog.append({
1060
+ type: 'collaboration.approval.requested',
1061
+ project_alias: projectContext.projectAlias,
1062
+ actor_id: context.actor_id,
1063
+ operation_class: operationClass,
1064
+ });
1065
+ return;
1066
+ }
1067
+ }
1068
+ catch { /* trust enforcement is best-effort */ }
1069
+ // Token quota enforcement
1070
+ if (projectContext.project.daily_token_quota) {
1071
+ try {
1072
+ const costSummary = await this.runStateStore.getCostSummary(24);
1073
+ const projectUsage = costSummary.by_project[projectContext.projectAlias];
1074
+ if (projectUsage) {
1075
+ const usedTokens = projectUsage.input_tokens + projectUsage.output_tokens;
1076
+ if (usedTokens > projectContext.project.daily_token_quota) {
1077
+ await this.sendTextReply(context.chat_id, `\u26a0\ufe0f 项目 ${projectContext.projectAlias} 已达到每日 token 额度 (${usedTokens}/${projectContext.project.daily_token_quota}),请联系管理员调整。`, context.message_id, context.text);
1078
+ return;
1079
+ }
1080
+ }
1081
+ }
1082
+ catch { /* quota check is best-effort */ }
1083
+ }
1084
+ const scheduled = await this.scheduleProjectExecution(projectContext, {
1085
+ chatId: context.chat_id,
1086
+ actorId: context.actor_id,
1087
+ prompt,
1088
+ }, async (runId) => {
1089
+ await this.executePrompt({
1090
+ runId,
1091
+ chatId: context.chat_id,
1092
+ actorId: context.actor_id,
1093
+ tenantKey: context.tenant_key,
1094
+ projectAlias: projectContext.projectAlias,
1095
+ project: projectContext.project,
1096
+ prompt,
1097
+ incomingMessage: resolvedContext,
1098
+ sessionKey: projectContext.sessionKey,
1099
+ queueKey: projectContext.queueKey,
1100
+ replyToMessageId: context.message_id,
1101
+ });
1102
+ });
1103
+ try {
1104
+ await this.sendInitialRunLifecycleReply({
1105
+ chatId: context.chat_id,
1106
+ projectAlias: projectContext.projectAlias,
1107
+ runId: scheduled.runId,
1108
+ queued: scheduled.queued,
1109
+ replyToMessageId: context.message_id,
1110
+ originalText: context.text,
1111
+ });
1112
+ }
1113
+ finally {
1114
+ scheduled.release();
1115
+ }
1116
+ await scheduled.completion;
1117
+ }
1118
+ async handleStatusCommand(context, selectionKey, detail = false) {
1119
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
1120
+ const activeRun = await this.runStateStore.getLatestVisibleRun(projectContext.queueKey);
1121
+ const conversation = await this.sessionStore.getConversation(projectContext.sessionKey);
1122
+ if (!conversation && !activeRun) {
1123
+ await this.sendTextReply(context.chat_id, `项目 ${projectContext.projectAlias} 还没有会话。发送任意文本即可开始。`, context.message_id, context.text);
1124
+ return;
1125
+ }
1126
+ if (this.config.service.reply_mode === 'card') {
1127
+ await this.sendCardReply(context.chat_id, this.buildStatusCardFromConversation(projectContext.projectAlias, projectContext.sessionKey, conversation, activeRun, context.chat_id), context.message_id);
1128
+ return;
1129
+ }
1130
+ const body = detail
1131
+ ? await this.buildDetailedStatusText(projectContext.projectAlias, projectContext.sessionKey, conversation, activeRun)
1132
+ : await this.buildStatusText(projectContext.projectAlias, conversation, activeRun);
1133
+ // Append collaboration status section
1134
+ const trustLevelLabels = {
1135
+ observe: '🔍 观察', suggest: '💡 建议', execute: '⚡ 执行', autonomous: '🚀 自主',
1136
+ };
1137
+ const trustState = await this.trustStore.getOrCreate(projectContext.projectAlias);
1138
+ const allRuns = await this.runStateStore.listRuns();
1139
+ const activeActorIds = new Set(allRuns
1140
+ .filter((r) => (r.status === 'running' || r.status === 'queued') && r.actor_id)
1141
+ .map((r) => r.actor_id));
1142
+ const pendingHandoffs = (await this.handoffStore.listHandoffs()).filter((h) => h.status === 'pending');
1143
+ const pendingReviews = (await this.handoffStore.listReviews()).filter((r) => r.status === 'pending');
1144
+ const collabSection = [
1145
+ '',
1146
+ '协作状态:',
1147
+ ` 信任等级: ${trustLevelLabels[trustState.current_level] ?? trustState.current_level}`,
1148
+ ` 团队活跃: ${activeActorIds.size} 人在线`,
1149
+ ` 待交接: ${pendingHandoffs.length} / 待评审: ${pendingReviews.length}`,
1150
+ ].join('\n');
1151
+ await this.sendTextReply(context.chat_id, body + collabSection, context.message_id, context.text);
1152
+ }
1153
+ async handleNewCommand(context, selectionKey) {
1154
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
1155
+ if (!this.canControlProjectSessions(context.chat_id, projectContext.projectAlias)) {
1156
+ await this.sendTextReply(context.chat_id, `当前 chat_id 无权为项目 ${projectContext.projectAlias} 新开会话。至少需要 ${describeMinimumRole('operator')} 权限。`, context.message_id, context.text);
1157
+ return;
1158
+ }
1159
+ await this.sessionStore.clearActiveProjectSession(projectContext.sessionKey, projectContext.projectAlias);
1160
+ await this.auditLog.append({
1161
+ type: 'session.reset',
1162
+ chat_id: context.chat_id,
1163
+ actor_id: context.actor_id,
1164
+ project_alias: projectContext.projectAlias,
1165
+ conversation_key: projectContext.sessionKey,
1166
+ });
1167
+ await this.sendTextReply(context.chat_id, `已为项目 ${projectContext.projectAlias} 切换到新会话模式。下一条消息会新开一轮。`, context.message_id, context.text);
1168
+ }
1169
+ async handleCancelCommand(context, selectionKey) {
1170
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
1171
+ if (!this.canCancelProjectRuns(context.chat_id, projectContext.projectAlias)) {
1172
+ await this.sendTextReply(context.chat_id, `当前 chat_id 无权取消项目 ${projectContext.projectAlias} 的运行。至少需要 ${describeMinimumRole('operator')} 权限。`, context.message_id, context.text);
1173
+ return;
1174
+ }
1175
+ const cancelled = await this.cancelActiveRun(projectContext.queueKey, 'user');
1176
+ await this.sendTextReply(context.chat_id, cancelled ? `已提交取消请求: ${projectContext.projectAlias}` : `当前项目 ${projectContext.projectAlias} 没有活动中的运行。`, context.message_id, context.text);
1177
+ }
1178
+ async handleSessionCommand(context, selectionKey, action, threadId) {
1179
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
1180
+ const sessions = await this.sessionStore.listProjectSessions(projectContext.sessionKey, projectContext.projectAlias);
1181
+ const activeSessionId = (await this.sessionStore.getConversation(projectContext.sessionKey))?.projects[projectContext.projectAlias]?.thread_id;
1182
+ switch (action) {
1183
+ case 'list': {
1184
+ const listing = await listSharedBridgeSessions(this.config, this.sessionStore, {
1185
+ chatId: context.chat_id,
1186
+ actorId: context.actor_id,
1187
+ tenantKey: context.tenant_key,
1188
+ projectAlias: projectContext.projectAlias,
1189
+ });
1190
+ await this.sendTextReply(context.chat_id, listing.text, context.message_id, context.text);
1191
+ return;
1192
+ }
1193
+ case 'use': {
1194
+ if (!this.canControlProjectSessions(context.chat_id, projectContext.projectAlias)) {
1195
+ await this.sendTextReply(context.chat_id, `当前 chat_id 无权切换项目 ${projectContext.projectAlias} 的会话。至少需要 ${describeMinimumRole('operator')} 权限。`, context.message_id, context.text);
1196
+ return;
1197
+ }
1198
+ if (!threadId) {
1199
+ await this.sendTextReply(context.chat_id, '用法: /session use <thread_id>', context.message_id, context.text);
1200
+ return;
1201
+ }
1202
+ await this.sessionStore.setActiveProjectSession(projectContext.sessionKey, projectContext.projectAlias, threadId);
1203
+ await this.sendTextReply(context.chat_id, `已切换到会话: ${threadId}`, context.message_id, context.text);
1204
+ return;
1205
+ }
1206
+ case 'new': {
1207
+ if (!this.canControlProjectSessions(context.chat_id, projectContext.projectAlias)) {
1208
+ await this.sendTextReply(context.chat_id, `当前 chat_id 无权为项目 ${projectContext.projectAlias} 新开会话。至少需要 ${describeMinimumRole('operator')} 权限。`, context.message_id, context.text);
1209
+ return;
1210
+ }
1211
+ await this.sessionStore.clearActiveProjectSession(projectContext.sessionKey, projectContext.projectAlias);
1212
+ await this.sendTextReply(context.chat_id, '已切换为新会话模式。下一条消息会新开会话。', context.message_id, context.text);
1213
+ return;
1214
+ }
1215
+ case 'drop': {
1216
+ if (!this.canControlProjectSessions(context.chat_id, projectContext.projectAlias)) {
1217
+ await this.sendTextReply(context.chat_id, `当前 chat_id 无权删除项目 ${projectContext.projectAlias} 的会话。至少需要 ${describeMinimumRole('operator')} 权限。`, context.message_id, context.text);
1218
+ return;
1219
+ }
1220
+ const targetThreadId = threadId ?? activeSessionId;
1221
+ if (!targetThreadId) {
1222
+ await this.sendTextReply(context.chat_id, '没有可删除的会话。', context.message_id, context.text);
1223
+ return;
1224
+ }
1225
+ await this.sessionStore.dropProjectSession(projectContext.sessionKey, projectContext.projectAlias, targetThreadId);
1226
+ await this.sendTextReply(context.chat_id, `已删除会话: ${targetThreadId}`, context.message_id, context.text);
1227
+ return;
1228
+ }
1229
+ case 'adopt': {
1230
+ if (!this.canControlProjectSessions(context.chat_id, projectContext.projectAlias)) {
1231
+ await this.sendTextReply(context.chat_id, `当前 chat_id 无权接管项目 ${projectContext.projectAlias} 的会话。至少需要 ${describeMinimumRole('operator')} 权限。`, context.message_id, context.text);
1232
+ return;
1233
+ }
1234
+ await this.handleSessionAdoptCommand(context, projectContext, threadId);
1235
+ return;
1236
+ }
1237
+ }
1238
+ }
1239
+ // ── Direction 1: Team Awareness ──
1240
+ async handleTeamCommand(context) {
1241
+ const runs = await this.runStateStore.listRuns();
1242
+ const activities = buildTeamActivityView(runs);
1243
+ const text = formatTeamView(activities);
1244
+ await this.sendTextReply(context.chat_id, text, context.message_id, context.text);
1245
+ }
1246
+ // ── Direction 2: Knowledge Loop ──
1247
+ async handleLearnCommand(context, selectionKey, value) {
1248
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
1249
+ const input = buildLearnInput(value, projectContext.projectAlias, context.actor_id, context.chat_id);
1250
+ await this.memoryStore.saveProjectMemory({
1251
+ project_alias: input.project_alias,
1252
+ title: input.title,
1253
+ content: input.content,
1254
+ tags: input.tags,
1255
+ source: input.source,
1256
+ created_by: context.actor_id,
1257
+ });
1258
+ await this.auditLog.append({
1259
+ type: 'collaboration.knowledge.learned',
1260
+ project_alias: input.project_alias,
1261
+ actor_id: context.actor_id,
1262
+ title: input.title,
1263
+ });
1264
+ await this.sendTextReply(context.chat_id, `💡 团队知识已记录: "${input.title}"\n项目: ${input.project_alias}`, context.message_id, context.text);
1265
+ }
1266
+ async handleRecallCommand(context, selectionKey, query) {
1267
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
1268
+ const memories = await this.memoryStore.searchMemories({ scope: 'project', project_alias: projectContext.projectAlias }, query, 10);
1269
+ const text = formatRecallResults(memories, query);
1270
+ await this.sendTextReply(context.chat_id, text, context.message_id, context.text);
1271
+ }
1272
+ // ── Direction 3: Handoff & Review ──
1273
+ async handleHandoffCommand(context, selectionKey, summary) {
1274
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
1275
+ const conversation = await this.sessionStore.getConversation(projectContext.sessionKey);
1276
+ const projectState = conversation?.projects[projectContext.projectAlias];
1277
+ const record = createHandoff({
1278
+ from_actor_id: context.actor_id ?? 'unknown',
1279
+ from_actor_name: context.actor_name,
1280
+ project_alias: projectContext.projectAlias,
1281
+ conversation_key: projectContext.sessionKey,
1282
+ thread_id: projectState?.active_thread_id ?? projectState?.thread_id,
1283
+ summary: summary ?? '会话交接',
1284
+ last_prompt: projectState?.last_prompt,
1285
+ last_response_excerpt: projectState?.last_response_excerpt,
1286
+ });
1287
+ await this.handoffStore.addHandoff(record);
1288
+ await this.auditLog.append({
1289
+ type: 'collaboration.handoff.created',
1290
+ handoff_id: record.id,
1291
+ from_actor_id: record.from_actor_id,
1292
+ project_alias: record.project_alias,
1293
+ });
1294
+ await this.sendTextReply(context.chat_id, formatHandoff(record), context.message_id, context.text);
1295
+ }
1296
+ async handlePickupCommand(context, selectionKey, id) {
1297
+ let handoff = id
1298
+ ? await this.handoffStore.updateHandoff(id, {}) // just to find it
1299
+ : await this.handoffStore.getPendingHandoffForActor(context.actor_id ?? '', undefined);
1300
+ if (id) {
1301
+ handoff = await this.handoffStore.getPendingHandoff();
1302
+ if (handoff && !handoff.id.startsWith(id)) {
1303
+ handoff = null;
1304
+ }
1305
+ }
1306
+ if (!handoff || handoff.status !== 'pending') {
1307
+ await this.sendTextReply(context.chat_id, '没有找到待接手的交接任务。', context.message_id, context.text);
1308
+ return;
1309
+ }
1310
+ const accepted = acceptHandoff(handoff, context.actor_id ?? 'unknown');
1311
+ await this.handoffStore.updateHandoff(handoff.id, {
1312
+ status: 'accepted',
1313
+ accepted_at: accepted.accepted_at,
1314
+ accepted_by: accepted.accepted_by,
1315
+ });
1316
+ // Adopt the session if there's a thread_id
1317
+ if (handoff.thread_id) {
1318
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
1319
+ await this.sessionStore.setActiveProjectSession(projectContext.sessionKey, handoff.project_alias, handoff.thread_id);
1320
+ }
1321
+ await this.auditLog.append({
1322
+ type: 'collaboration.handoff.accepted',
1323
+ handoff_id: handoff.id,
1324
+ accepted_by: context.actor_id,
1325
+ project_alias: handoff.project_alias,
1326
+ });
1327
+ await this.sendTextReply(context.chat_id, `✅ 已接手 ${handoff.from_actor_name ?? handoff.from_actor_id} 的交接任务 [${handoff.project_alias}]\n摘要: ${handoff.summary}`, context.message_id, context.text);
1328
+ }
1329
+ async handleReviewCommand(context, selectionKey) {
1330
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
1331
+ const runs = await this.runStateStore.listRuns();
1332
+ const latestRun = runs.find((r) => r.project_alias === projectContext.projectAlias && (r.status === 'success' || r.status === 'failure'));
1333
+ if (!latestRun) {
1334
+ await this.sendTextReply(context.chat_id, '没有找到最近的运行结果可供评审。', context.message_id, context.text);
1335
+ return;
1336
+ }
1337
+ const review = createReview({
1338
+ run_id: latestRun.run_id,
1339
+ project_alias: projectContext.projectAlias,
1340
+ chat_id: context.chat_id,
1341
+ actor_id: context.actor_id ?? 'unknown',
1342
+ content_excerpt: latestRun.prompt_excerpt,
1343
+ });
1344
+ await this.handoffStore.addReview(review);
1345
+ await this.auditLog.append({
1346
+ type: 'collaboration.review.created',
1347
+ review_id: review.id,
1348
+ run_id: review.run_id,
1349
+ project_alias: review.project_alias,
1350
+ });
1351
+ await this.sendTextReply(context.chat_id, formatReview(review), context.message_id, context.text);
1352
+ }
1353
+ async handleApproveCommand(context, comment) {
1354
+ const pending = await this.handoffStore.getPendingReview(context.chat_id);
1355
+ if (!pending) {
1356
+ await this.sendTextReply(context.chat_id, '当前没有待评审的内容。', context.message_id, context.text);
1357
+ return;
1358
+ }
1359
+ const resolved = resolveReview(pending, 'approved', context.actor_id ?? 'unknown', comment);
1360
+ await this.handoffStore.updateReview(pending.id, {
1361
+ status: 'approved',
1362
+ reviewer_id: resolved.reviewer_id,
1363
+ review_comment: resolved.review_comment,
1364
+ resolved_at: resolved.resolved_at,
1365
+ });
1366
+ await this.auditLog.append({
1367
+ type: 'collaboration.review.approved',
1368
+ review_id: pending.id,
1369
+ reviewer_id: context.actor_id,
1370
+ });
1371
+ await this.sendTextReply(context.chat_id, formatReviewResult(resolved), context.message_id, context.text);
1372
+ }
1373
+ async handleRejectCommand(context, reason) {
1374
+ const pending = await this.handoffStore.getPendingReview(context.chat_id);
1375
+ if (!pending) {
1376
+ await this.sendTextReply(context.chat_id, '当前没有待评审的内容。', context.message_id, context.text);
1377
+ return;
1378
+ }
1379
+ const resolved = resolveReview(pending, 'rejected', context.actor_id ?? 'unknown', reason);
1380
+ await this.handoffStore.updateReview(pending.id, {
1381
+ status: 'rejected',
1382
+ reviewer_id: resolved.reviewer_id,
1383
+ review_comment: resolved.review_comment,
1384
+ resolved_at: resolved.resolved_at,
1385
+ });
1386
+ await this.auditLog.append({
1387
+ type: 'collaboration.review.rejected',
1388
+ review_id: pending.id,
1389
+ reviewer_id: context.actor_id,
1390
+ reason,
1391
+ });
1392
+ await this.sendTextReply(context.chat_id, formatReviewResult(resolved), context.message_id, context.text);
1393
+ }
1394
+ // ── Direction 4: Insights ──
1395
+ async handleInsightsCommand(context) {
1396
+ const runs = await this.runStateStore.listRuns();
1397
+ const auditEvents = await this.auditLog.tail(500);
1398
+ const insights = analyzeTeamHealth(runs, auditEvents);
1399
+ const text = formatInsightsReport(insights);
1400
+ await this.sendTextReply(context.chat_id, text, context.message_id, context.text);
1401
+ }
1402
+ // ── Direction 5: Trust ──
1403
+ async handleTrustCommand(context, selectionKey, action, level) {
1404
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
1405
+ if (action === 'set' && level) {
1406
+ const TRUST_ORDER = ['observe', 'suggest', 'execute', 'autonomous'];
1407
+ const validLevels = [...TRUST_ORDER];
1408
+ const state = await this.trustStore.getOrCreate(projectContext.projectAlias);
1409
+ // Handle relative promote/demote from natural language
1410
+ let resolvedLevel = level;
1411
+ if (level === '_promote') {
1412
+ const idx = TRUST_ORDER.indexOf(state.current_level);
1413
+ if (idx >= TRUST_ORDER.length - 1) {
1414
+ await this.sendTextReply(context.chat_id, `已经是最高信任等级 (${state.current_level}),无法继续提升。`, context.message_id, context.text);
1415
+ return;
1416
+ }
1417
+ resolvedLevel = TRUST_ORDER[idx + 1];
1418
+ }
1419
+ else if (level === '_demote') {
1420
+ const idx = TRUST_ORDER.indexOf(state.current_level);
1421
+ if (idx <= 0) {
1422
+ await this.sendTextReply(context.chat_id, `已经是最低信任等级 (${state.current_level}),无法继续降低。`, context.message_id, context.text);
1423
+ return;
1424
+ }
1425
+ resolvedLevel = TRUST_ORDER[idx - 1];
1426
+ }
1427
+ if (!validLevels.includes(resolvedLevel)) {
1428
+ await this.sendTextReply(context.chat_id, `无效的信任等级。有效值: ${validLevels.join(', ')}`, context.message_id, context.text);
1429
+ return;
1430
+ }
1431
+ state.current_level = resolvedLevel;
1432
+ state.last_evaluated_at = new Date().toISOString();
1433
+ await this.trustStore.update(projectContext.projectAlias, state);
1434
+ this.metrics?.recordTrustLevel(projectContext.projectAlias, resolvedLevel);
1435
+ await this.auditLog.append({
1436
+ type: 'collaboration.trust.set',
1437
+ project_alias: projectContext.projectAlias,
1438
+ actor_id: context.actor_id,
1439
+ level,
1440
+ });
1441
+ await this.sendTextReply(context.chat_id, `🛡️ 项目 ${projectContext.projectAlias} 的信任等级已设置为: ${level}`, context.message_id, context.text);
1442
+ return;
1443
+ }
1444
+ const state = await this.trustStore.getOrCreate(projectContext.projectAlias);
1445
+ await this.sendTextReply(context.chat_id, formatTrustState(state), context.message_id, context.text);
1446
+ }
1447
+ // ── Team Digest ──
1448
+ async handleDigestCommand(context) {
1449
+ const period = createDigestPeriod(this.config.service.team_digest_interval_hours);
1450
+ const runs = await this.runStateStore.listRuns();
1451
+ const memories = this.config.service.memory_enabled
1452
+ ? await this.memoryStore.listRecentMemories({ scope: 'project', project_alias: '' }, 100)
1453
+ : [];
1454
+ const auditEvents = await this.auditLog.tail(500);
1455
+ const digest = buildTeamDigest(runs, memories, auditEvents, period);
1456
+ const text = formatTeamDigest(digest);
1457
+ await this.sendTextReply(context.chat_id, text, context.message_id, context.text);
1458
+ }
1459
+ // ── Direction 6: Timeline ──
1460
+ async handleTimelineCommand(context, selectionKey, projectArg) {
1461
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
1462
+ const projectAlias = projectArg ?? projectContext.projectAlias;
1463
+ const runs = await this.runStateStore.listRuns();
1464
+ const auditEvents = await this.auditLog.tail(200);
1465
+ const memories = this.config.service.memory_enabled
1466
+ ? await this.memoryStore.listRecentMemories({ scope: 'project', project_alias: projectAlias }, 20)
1467
+ : [];
1468
+ const timeline = buildProjectTimeline(runs, memories, auditEvents, projectAlias, 20);
1469
+ const text = formatTimeline(timeline);
1470
+ await this.sendTextReply(context.chat_id, text, context.message_id, context.text);
1471
+ }
1472
+ async handleAdminCommand(context, selectionKey, command) {
1473
+ const runtimeConfigPath = this.runtimeControl?.configPath;
1474
+ const currentProjectAlias = await this.resolveProjectAlias(selectionKey);
1475
+ const globalAdmin = this.isAdminChat(context.chat_id);
1476
+ const globalConfigAdmin = this.canMutateRuntimeConfig(context.chat_id);
1477
+ const serviceObserver = this.canObserveService(context.chat_id) || this.canObserveRuns(context.chat_id);
1478
+ const serviceRestarter = this.canRestartService(context.chat_id);
1479
+ const projectAdminAliases = this.getAuthorizedProjectAliases(context.chat_id, 'admin');
1480
+ const projectOperatorAliases = this.getAuthorizedProjectAliases(context.chat_id, 'operator');
1481
+ const canAccess = globalAdmin ||
1482
+ this.canAccessAdminCommand(command, currentProjectAlias, projectAdminAliases, projectOperatorAliases, {
1483
+ globalConfigAdmin,
1484
+ serviceObserver,
1485
+ serviceRestarter,
1486
+ });
1487
+ if (!canAccess) {
1488
+ await this.sendTextReply(context.chat_id, '当前 chat_id 没有足够权限。请先在全局或项目级角色列表中授予 operator/admin 权限。', context.message_id, context.text);
1489
+ return;
1490
+ }
1491
+ if (!runtimeConfigPath && this.commandRequiresWritableConfig(command)) {
1492
+ await this.sendTextReply(context.chat_id, '当前运行实例没有可写配置路径,无法执行管理员动态操作。', context.message_id, context.text);
1493
+ return;
1494
+ }
1495
+ if (command.resource === 'service') {
1496
+ if (command.action === 'runs') {
1497
+ await this.sendTextReply(context.chat_id, await this.buildAdminRunsText(globalAdmin || serviceObserver ? undefined : new Set(projectOperatorAliases)), context.message_id, context.text);
1498
+ return;
1499
+ }
1500
+ if (command.action === 'restart') {
1501
+ if (!(globalAdmin || serviceRestarter)) {
1502
+ await this.sendTextReply(context.chat_id, '当前 chat_id 无权重启服务。', context.message_id, context.text);
1503
+ return;
1504
+ }
1505
+ await this.sendTextReply(context.chat_id, '配置已保存,正在重启服务。预计数秒内恢复。', context.message_id, context.text);
1506
+ this.logger.warn({ chatId: context.chat_id, actorId: context.actor_id }, 'Restart requested by Feishu admin');
1507
+ await this.appendAdminAudit({
1508
+ type: 'admin.service.restart',
1509
+ chat_id: context.chat_id,
1510
+ actor_id: context.actor_id,
1511
+ config_path: runtimeConfigPath,
1512
+ });
1513
+ await this.runtimeControl?.restart?.();
1514
+ return;
1515
+ }
1516
+ await this.sendTextReply(context.chat_id, await this.buildAdminStatusText(), context.message_id, context.text);
1517
+ return;
1518
+ }
1519
+ if (command.resource === 'config') {
1520
+ await this.handleAdminConfigCommand(context, command);
1521
+ return;
1522
+ }
1523
+ if (command.resource === 'project') {
1524
+ if (command.action === 'list') {
1525
+ await this.sendTextReply(context.chat_id, this.buildProjectsAdminText(globalAdmin || serviceObserver ? undefined : new Set(projectOperatorAliases)), context.message_id, context.text);
1526
+ return;
1527
+ }
1528
+ if (command.action === 'add' || command.action === 'create') {
1529
+ if (!(globalAdmin || globalConfigAdmin)) {
1530
+ await this.sendTextReply(context.chat_id, '当前 chat_id 无权动态接入项目。', context.message_id, context.text);
1531
+ return;
1532
+ }
1533
+ if (!command.alias || !command.value) {
1534
+ await this.sendTextReply(context.chat_id, command.action === 'create' ? '用法: /admin project create <alias> <root>' : '用法: /admin project add <alias> <root>', context.message_id, context.text);
1535
+ return;
1536
+ }
1537
+ if (command.action === 'create' && this.config.projects[command.alias]) {
1538
+ await this.sendTextReply(context.chat_id, `项目已存在: ${command.alias}`, context.message_id, context.text);
1539
+ return;
1540
+ }
1541
+ const resolvedRoot = path.resolve(expandHomePath(command.value));
1542
+ const snapshot = await this.snapshotConfigForAdminMutation(context, `project.${command.action}`, `${command.alias} -> ${resolvedRoot}`);
1543
+ if (command.action === 'create') {
1544
+ await createProjectAlias({ configPath: runtimeConfigPath, alias: command.alias, root: command.value });
1545
+ }
1546
+ else {
1547
+ await bindProjectAlias({ configPath: runtimeConfigPath, alias: command.alias, root: command.value });
1548
+ }
1549
+ this.config.projects[command.alias] = {
1550
+ root: resolvedRoot,
1551
+ session_scope: 'chat',
1552
+ mention_required: true,
1553
+ knowledge_paths: [],
1554
+ wiki_space_ids: [],
1555
+ viewer_chat_ids: [],
1556
+ operator_chat_ids: [],
1557
+ admin_chat_ids: [],
1558
+ notification_chat_ids: [],
1559
+ session_operator_chat_ids: [],
1560
+ run_operator_chat_ids: [],
1561
+ config_admin_chat_ids: [],
1562
+ run_priority: 100,
1563
+ chat_rate_limit_window_seconds: 60,
1564
+ chat_rate_limit_max_runs: 20,
1565
+ };
1566
+ await this.sendTextReply(context.chat_id, `${command.action === 'create' ? '已创建并接入项目' : '已接入项目'}: ${command.alias}\n根目录: ${resolvedRoot}`, context.message_id, context.text);
1567
+ await this.appendAdminAudit({
1568
+ type: `admin.project.${command.action}`,
1569
+ chat_id: context.chat_id,
1570
+ actor_id: context.actor_id,
1571
+ project_alias: command.alias,
1572
+ root: resolvedRoot,
1573
+ snapshot_id: snapshot.id,
1574
+ });
1575
+ this.logger.info({ alias: command.alias, root: resolvedRoot, actorId: context.actor_id, created: command.action === 'create' }, command.action === 'create' ? 'Project created by Feishu admin' : 'Project added by Feishu admin');
1576
+ return;
1577
+ }
1578
+ if (command.action === 'remove') {
1579
+ if (!(globalAdmin || globalConfigAdmin)) {
1580
+ await this.sendTextReply(context.chat_id, '当前 chat_id 无权移除项目。', context.message_id, context.text);
1581
+ return;
1582
+ }
1583
+ if (!command.alias) {
1584
+ await this.sendTextReply(context.chat_id, '用法: /admin project remove <alias>', context.message_id, context.text);
1585
+ return;
1586
+ }
1587
+ if (this.config.service.default_project === command.alias) {
1588
+ await this.sendTextReply(context.chat_id, `不能移除默认项目: ${command.alias}。请先切换 service.default_project。`, context.message_id, context.text);
1589
+ return;
1590
+ }
1591
+ const snapshot = await this.snapshotConfigForAdminMutation(context, 'project.remove', command.alias);
1592
+ await removeProjectAlias(runtimeConfigPath, command.alias);
1593
+ delete this.config.projects[command.alias];
1594
+ await this.sendTextReply(context.chat_id, `已移除项目: ${command.alias}`, context.message_id, context.text);
1595
+ await this.appendAdminAudit({
1596
+ type: 'admin.project.remove',
1597
+ chat_id: context.chat_id,
1598
+ actor_id: context.actor_id,
1599
+ project_alias: command.alias,
1600
+ snapshot_id: snapshot.id,
1601
+ });
1602
+ this.logger.info({ alias: command.alias, actorId: context.actor_id }, 'Project removed by Feishu admin');
1603
+ return;
1604
+ }
1605
+ if (!command.alias || !command.field || !command.value) {
1606
+ await this.sendTextReply(context.chat_id, '用法: /admin project set <alias> <field> <value>', context.message_id, context.text);
1607
+ return;
1608
+ }
1609
+ if (!(globalAdmin || globalConfigAdmin) && !this.isProjectAdminChat(context.chat_id, command.alias)) {
1610
+ await this.sendTextReply(context.chat_id, `当前 chat_id 无权修改项目 ${command.alias}。`, context.message_id, context.text);
1611
+ return;
1612
+ }
1613
+ const patch = this.parseProjectPatch(command.field, command.value);
1614
+ if (!patch) {
1615
+ await this.sendTextReply(context.chat_id, '支持字段: root, profile, sandbox, session_scope, mention_required, description, viewer_chat_ids, operator_chat_ids, admin_chat_ids, session_operator_chat_ids, run_operator_chat_ids, config_admin_chat_ids, download_dir, temp_dir, cache_dir, log_dir, run_priority, chat_rate_limit_window_seconds, chat_rate_limit_max_runs', context.message_id, context.text);
1616
+ return;
1617
+ }
1618
+ const snapshot = await this.snapshotConfigForAdminMutation(context, 'project.set', `${command.alias}.${command.field}=${command.value}`);
1619
+ const nextProject = await updateProjectConfig(runtimeConfigPath, command.alias, patch);
1620
+ this.config.projects[command.alias] = {
1621
+ ...this.requireProject(command.alias),
1622
+ ...nextProject,
1623
+ };
1624
+ await this.sendTextReply(context.chat_id, `已更新项目 ${command.alias}\n字段: ${command.field}\n值: ${command.value}`, context.message_id, context.text);
1625
+ await this.appendAdminAudit({
1626
+ type: 'admin.project.set',
1627
+ chat_id: context.chat_id,
1628
+ actor_id: context.actor_id,
1629
+ project_alias: command.alias,
1630
+ field: command.field,
1631
+ value: command.value,
1632
+ snapshot_id: snapshot.id,
1633
+ });
1634
+ this.logger.info({ alias: command.alias, field: command.field, actorId: context.actor_id }, 'Project config updated by Feishu admin');
1635
+ return;
1636
+ }
1637
+ if (command.action === 'list' || command.action === 'status') {
1638
+ await this.sendTextReply(context.chat_id, this.buildAdminListText(command.resource), context.message_id, context.text);
1639
+ return;
1640
+ }
1641
+ if (!command.value) {
1642
+ await this.sendTextReply(context.chat_id, `用法: /admin ${command.resource} ${command.action} <chat_id>`, context.message_id, context.text);
1643
+ return;
1644
+ }
1645
+ const snapshot = await this.snapshotConfigForAdminMutation(context, `${command.resource}.${command.action}`, command.value);
1646
+ const { section, key } = resolveAdminListTarget(command.resource);
1647
+ const nextValues = await updateStringList(runtimeConfigPath, section, key, command.value, command.action);
1648
+ this.applyAdminListValues(command.resource, nextValues);
1649
+ await this.sendTextReply(context.chat_id, `已${command.action === 'add' ? '添加' : '移除'} ${command.resource}:\n${command.value}`, context.message_id, context.text);
1650
+ await this.appendAdminAudit({
1651
+ type: `admin.${command.resource}.${command.action}`,
1652
+ chat_id: context.chat_id,
1653
+ actor_id: context.actor_id,
1654
+ value: command.value,
1655
+ snapshot_id: snapshot.id,
1656
+ });
1657
+ this.logger.info({ resource: command.resource, action: command.action, value: command.value, actorId: context.actor_id }, 'Feishu access list updated by admin');
1658
+ }
1659
+ async handleSessionAdoptCommand(context, projectContext, target) {
1660
+ const adoption = await adoptSharedProjectSession(this.config, this.sessionStore, this.codexSessionIndex, {
1661
+ chatId: context.chat_id,
1662
+ actorId: context.actor_id,
1663
+ tenantKey: context.tenant_key,
1664
+ projectAlias: projectContext.projectAlias,
1665
+ }, target);
1666
+ if (adoption.structured.adopted) {
1667
+ await this.auditLog.append({
1668
+ type: 'session.adopted',
1669
+ chat_id: context.chat_id,
1670
+ actor_id: context.actor_id,
1671
+ project_alias: projectContext.projectAlias,
1672
+ conversation_key: projectContext.sessionKey,
1673
+ thread_id: adoption.structured.adopted.sessionId,
1674
+ source_cwd: adoption.structured.adopted.cwd,
1675
+ source: adoption.structured.adopted.source,
1676
+ match_kind: adoption.structured.adopted.matchKind,
1677
+ backend: adoption.structured.adopted.backend,
1678
+ });
1679
+ }
1680
+ await this.sendTextReply(context.chat_id, adoption.text, context.message_id, context.text);
1681
+ }
1682
+ async handleBackendCommand(context, selectionKey, name) {
1683
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
1684
+ if (!name) {
1685
+ const sessionOverride = await this.sessionStore.getProjectBackend(projectContext.sessionKey, projectContext.projectAlias);
1686
+ const effectiveName = resolveProjectBackendName(this.config, projectContext.projectAlias, sessionOverride);
1687
+ const source = sessionOverride
1688
+ ? '会话级覆盖'
1689
+ : this.config.projects[projectContext.projectAlias]?.backend
1690
+ ? '项目配置'
1691
+ : '全局默认';
1692
+ await this.sendTextReply(context.chat_id, `项目: ${projectContext.projectAlias}\n当前后端: ${effectiveName} (${source})`, context.message_id, context.text);
1693
+ return;
1694
+ }
1695
+ const normalized = name.toLowerCase();
1696
+ if (normalized !== 'codex' && normalized !== 'claude') {
1697
+ await this.sendTextReply(context.chat_id, `未知后端: ${name}\n可选值: codex | claude`, context.message_id, context.text);
1698
+ return;
1699
+ }
1700
+ const backendName = normalized;
1701
+ await this.sessionStore.setProjectBackend(projectContext.sessionKey, projectContext.projectAlias, backendName);
1702
+ await this.auditLog.append({
1703
+ type: 'backend.switched',
1704
+ chat_id: context.chat_id,
1705
+ actor_id: context.actor_id,
1706
+ project_alias: projectContext.projectAlias,
1707
+ conversation_key: projectContext.sessionKey,
1708
+ backend: backendName,
1709
+ });
1710
+ const label = backendName === 'claude' ? 'Claude Code' : 'Codex';
1711
+ await this.sendTextReply(context.chat_id, `项目 ${projectContext.projectAlias} 已切换到 ${label} 后端。\n下一条消息将使用 ${label} 执行。`, context.message_id, context.text);
1712
+ }
1713
+ async handleMemoryCommand(context, selectionKey, action, scope, value, filters) {
1714
+ if (!this.config.service.memory_enabled) {
1715
+ await this.sendTextReply(context.chat_id, '当前未启用记忆功能。请在配置里设置 `service.memory_enabled = true`。', context.message_id, context.text);
1716
+ return;
1717
+ }
1718
+ try {
1719
+ const explicitExpiredCleanup = action === 'forget' && value?.trim() === 'all-expired';
1720
+ if (!explicitExpiredCleanup) {
1721
+ await this.memoryStore.cleanupExpiredMemories();
1722
+ }
1723
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
1724
+ const conversation = await this.sessionStore.getConversation(projectContext.sessionKey);
1725
+ const activeThreadId = conversation?.projects[projectContext.projectAlias]?.thread_id;
1726
+ const groupMemoryAvailable = this.config.service.memory_group_enabled && context.chat_type === 'group';
1727
+ if (action === 'status') {
1728
+ if (scope === 'group') {
1729
+ const target = this.resolveMemoryTarget(context, 'group');
1730
+ const [count, pinnedCount] = await Promise.all([
1731
+ this.memoryStore.countGroupMemories(projectContext.projectAlias, target.chatId),
1732
+ this.memoryStore.countPinnedGroupMemories(projectContext.projectAlias, target.chatId),
1733
+ ]);
1734
+ await this.sendTextReply(context.chat_id, [
1735
+ `项目: ${projectContext.projectAlias}`,
1736
+ `群共享记忆数: ${count}`,
1737
+ `Pinned 群共享记忆数: ${pinnedCount}`,
1738
+ `群 chat_id: ${target.chatId}`,
1739
+ ].join('\n'), context.message_id, context.text);
1740
+ return;
1741
+ }
1742
+ const [count, pinnedCount, threadSummary, groupCount, groupPinnedCount] = await Promise.all([
1743
+ this.memoryStore.countProjectMemories(projectContext.projectAlias),
1744
+ this.memoryStore.countPinnedProjectMemories(projectContext.projectAlias),
1745
+ activeThreadId ? this.memoryStore.getThreadSummary(projectContext.sessionKey, projectContext.projectAlias, activeThreadId) : Promise.resolve(null),
1746
+ groupMemoryAvailable ? this.memoryStore.countGroupMemories(projectContext.projectAlias, context.chat_id) : Promise.resolve(0),
1747
+ groupMemoryAvailable ? this.memoryStore.countPinnedGroupMemories(projectContext.projectAlias, context.chat_id) : Promise.resolve(0),
1748
+ ]);
1749
+ await this.sendTextReply(context.chat_id, [
1750
+ `项目: ${projectContext.projectAlias}`,
1751
+ `项目记忆数: ${count}`,
1752
+ `Pinned 项目记忆数: ${pinnedCount}`,
1753
+ ...(groupMemoryAvailable ? [`群共享记忆数: ${groupCount}`, `Pinned 群共享记忆数: ${groupPinnedCount}`] : []),
1754
+ `当前会话: ${activeThreadId ?? '未开始'}`,
1755
+ '',
1756
+ threadSummary?.summary ?? '当前没有 thread summary。',
1757
+ ].join('\n'), context.message_id, context.text);
1758
+ return;
1759
+ }
1760
+ if (action === 'stats') {
1761
+ const target = this.resolveMemoryTarget(context, scope);
1762
+ const stats = await this.memoryStore.getMemoryStats({
1763
+ scope: target.scope,
1764
+ project_alias: projectContext.projectAlias,
1765
+ chat_id: target.chatId,
1766
+ });
1767
+ await this.sendTextReply(context.chat_id, [
1768
+ `项目: ${projectContext.projectAlias}`,
1769
+ `${target.label}统计:`,
1770
+ `active_count: ${stats.active_count}`,
1771
+ `expired_count: ${stats.expired_count}`,
1772
+ `pinned_count: ${stats.pinned_count}`,
1773
+ `archived_count: ${stats.archived_count}`,
1774
+ `latest_accessed_at: ${stats.latest_accessed_at ?? '-'}`,
1775
+ `latest_updated_at: ${stats.latest_updated_at ?? '-'}`,
1776
+ `latest_archived_at: ${stats.latest_archived_at ?? '-'}`,
1777
+ ].join('\n'), context.message_id, context.text);
1778
+ return;
1779
+ }
1780
+ if (action === 'recent') {
1781
+ const target = this.resolveMemoryTarget(context, scope);
1782
+ const recent = await this.memoryStore.listRecentMemories({ scope: target.scope, project_alias: projectContext.projectAlias, chat_id: target.chatId }, this.config.service.memory_recent_limit, filters);
1783
+ if (recent.length === 0) {
1784
+ await this.sendTextReply(context.chat_id, [
1785
+ `项目: ${projectContext.projectAlias}`,
1786
+ `当前没有可展示的${target.label}。`,
1787
+ ...this.renderMemoryFilterLines(filters),
1788
+ ].join('\n'), context.message_id, context.text);
1789
+ return;
1790
+ }
1791
+ await this.sendTextReply(context.chat_id, [
1792
+ `项目: ${projectContext.projectAlias}`,
1793
+ `最近${target.label}:`,
1794
+ ...this.renderMemoryFilterLines(filters),
1795
+ '',
1796
+ ...recent.map((item, index) => [
1797
+ `${index + 1}. ${item.title}${item.pinned ? ' [pinned]' : ''}`,
1798
+ ` id: ${item.id}`,
1799
+ ` source: ${item.source}`,
1800
+ ...(item.created_by ? [` created_by: ${item.created_by}`] : []),
1801
+ ...(item.tags.length > 0 ? [` tags: ${item.tags.join(', ')}`] : []),
1802
+ ` updated_at: ${item.updated_at}`,
1803
+ ...(item.last_accessed_at ? [` last_accessed_at: ${item.last_accessed_at}`] : []),
1804
+ ...(item.expires_at ? [` expires_at: ${item.expires_at}`] : []),
1805
+ ` ${truncateExcerpt(item.content, 180)}`,
1806
+ ].join('\n')),
1807
+ ].join('\n'), context.message_id, context.text);
1808
+ return;
1809
+ }
1810
+ if (action === 'search') {
1811
+ if (!value?.trim()) {
1812
+ await this.sendTextReply(context.chat_id, '用法: /memory search [--tag <tag>] [--source <source>] [--created-by <actor_id>] <query>', context.message_id, context.text);
1813
+ return;
1814
+ }
1815
+ const target = this.resolveMemoryTarget(context, scope);
1816
+ const hits = await this.memoryStore.searchMemories({ scope: target.scope, project_alias: projectContext.projectAlias, chat_id: target.chatId }, value, this.config.service.memory_search_limit, filters);
1817
+ await this.auditLog.append({
1818
+ type: 'memory.search',
1819
+ chat_id: context.chat_id,
1820
+ actor_id: context.actor_id,
1821
+ project_alias: projectContext.projectAlias,
1822
+ scope: target.scope,
1823
+ query: value,
1824
+ result_count: hits.length,
1825
+ });
1826
+ if (hits.length === 0) {
1827
+ await this.sendTextReply(context.chat_id, [`项目: ${projectContext.projectAlias}`, `${target.label}搜索: ${value}`, ...this.renderMemoryFilterLines(filters), '未找到匹配记忆。'].join('\n'), context.message_id, context.text);
1828
+ return;
1829
+ }
1830
+ await this.sendTextReply(context.chat_id, [
1831
+ `项目: ${projectContext.projectAlias}`,
1832
+ `${target.label}搜索: ${value}`,
1833
+ ...this.renderMemoryFilterLines(filters),
1834
+ '',
1835
+ ...hits.map((hit, index) => [
1836
+ `${index + 1}. ${hit.title}${hit.pinned ? ' [pinned]' : ''}`,
1837
+ ` id: ${hit.id}`,
1838
+ ` source: ${hit.source}`,
1839
+ ...(hit.created_by ? [` created_by: ${hit.created_by}`] : []),
1840
+ ...(hit.tags.length > 0 ? [` tags: ${hit.tags.join(', ')}`] : []),
1841
+ ...(hit.last_accessed_at ? [` last_accessed_at: ${hit.last_accessed_at}`] : []),
1842
+ ` ${truncateExcerpt(hit.content, 180)}`,
1843
+ ].join('\n')),
1844
+ ].join('\n'), context.message_id, context.text);
1845
+ return;
1846
+ }
1847
+ if (action === 'pin' || action === 'unpin' || action === 'forget' || action === 'restore') {
1848
+ if (!value?.trim()) {
1849
+ const usage = action === 'forget'
1850
+ ? '用法: /memory forget <id> 或 /memory forget group <id>'
1851
+ : action === 'restore'
1852
+ ? '用法: /memory restore <id> 或 /memory restore group <id>'
1853
+ : `用法: /memory ${action} <id> 或 /memory ${action} group <id>`;
1854
+ await this.sendTextReply(context.chat_id, usage, context.message_id, context.text);
1855
+ return;
1856
+ }
1857
+ const target = this.resolveMemoryTarget(context, scope);
1858
+ const selector = { scope: target.scope, project_alias: projectContext.projectAlias, chat_id: target.chatId };
1859
+ if (action === 'forget' && value === 'all-expired') {
1860
+ const cleaned = await this.memoryStore.cleanupExpiredMemories(selector);
1861
+ await this.auditLog.append({
1862
+ type: 'memory.archive.expired',
1863
+ chat_id: context.chat_id,
1864
+ actor_id: context.actor_id,
1865
+ project_alias: projectContext.projectAlias,
1866
+ scope: target.scope,
1867
+ count: cleaned,
1868
+ });
1869
+ await this.sendTextReply(context.chat_id, `${target.label}已归档过期项: ${cleaned}`, context.message_id, context.text);
1870
+ return;
1871
+ }
1872
+ const existing = await this.memoryStore.getMemoryById(selector, value, { includeArchived: action === 'restore', includeExpired: action === 'restore' });
1873
+ if (!existing) {
1874
+ await this.sendTextReply(context.chat_id, `未找到可更新的${target.label} ID: ${value}`, context.message_id, context.text);
1875
+ return;
1876
+ }
1877
+ if (action === 'forget') {
1878
+ const archived = await this.memoryStore.archiveMemory(selector, value, { archived_by: context.actor_id, reason: 'manual' });
1879
+ if (archived) {
1880
+ await this.auditLog.append({
1881
+ type: 'memory.archive',
1882
+ chat_id: context.chat_id,
1883
+ actor_id: context.actor_id,
1884
+ project_alias: projectContext.projectAlias,
1885
+ scope: target.scope,
1886
+ memory_id: value,
1887
+ });
1888
+ }
1889
+ await this.sendTextReply(context.chat_id, archived
1890
+ ? [`${target.label}已归档: ${archived.title}`, `memory_id: ${archived.id}`, `可用 /memory restore${target.scope === 'group' ? ' group' : ''} ${archived.id} 恢复`].join('\n')
1891
+ : `未找到可归档的${target.label} ID: ${value}`, context.message_id, context.text);
1892
+ return;
1893
+ }
1894
+ if (action === 'restore') {
1895
+ const restored = await this.memoryStore.restoreMemory(selector, value, context.actor_id);
1896
+ if (restored) {
1897
+ await this.auditLog.append({
1898
+ type: 'memory.restore',
1899
+ chat_id: context.chat_id,
1900
+ actor_id: context.actor_id,
1901
+ project_alias: projectContext.projectAlias,
1902
+ scope: target.scope,
1903
+ memory_id: value,
1904
+ });
1905
+ }
1906
+ await this.sendTextReply(context.chat_id, restored ? `${target.label}已恢复: ${restored.title}\nmemory_id: ${restored.id}` : `未找到可恢复的${target.label} ID: ${value}`, context.message_id, context.text);
1907
+ return;
1908
+ }
1909
+ const pinned = action === 'pin';
1910
+ let agedOutMemoryTitle;
1911
+ let agedOutMemoryId;
1912
+ if (pinned && !existing.pinned) {
1913
+ const pinnedCount = await this.memoryStore.countPinnedMemories(selector);
1914
+ if (pinnedCount >= this.config.service.memory_max_pinned_per_scope) {
1915
+ if (this.config.service.memory_pin_overflow_strategy === 'age-out') {
1916
+ const oldest = await this.memoryStore.getOldestPinnedMemory(selector, this.config.service.memory_pin_age_basis);
1917
+ if (oldest && oldest.id !== existing.id) {
1918
+ await this.memoryStore.setMemoryPinned(selector, oldest.id, false);
1919
+ agedOutMemoryTitle = oldest.title;
1920
+ agedOutMemoryId = oldest.id;
1921
+ await this.auditLog.append({
1922
+ type: 'memory.pin.aged_out',
1923
+ chat_id: context.chat_id,
1924
+ actor_id: context.actor_id,
1925
+ project_alias: projectContext.projectAlias,
1926
+ scope: target.scope,
1927
+ memory_id: oldest.id,
1928
+ replaced_by: existing.id,
1929
+ });
1930
+ }
1931
+ else {
1932
+ await this.sendTextReply(context.chat_id, `${target.label}置顶数量已达上限 (${this.config.service.memory_max_pinned_per_scope})。请先取消置顶旧记录。`, context.message_id, context.text);
1933
+ return;
1934
+ }
1935
+ }
1936
+ else {
1937
+ await this.sendTextReply(context.chat_id, `${target.label}置顶数量已达上限 (${this.config.service.memory_max_pinned_per_scope})。请先取消置顶旧记录。`, context.message_id, context.text);
1938
+ return;
1939
+ }
1940
+ }
1941
+ }
1942
+ const updated = await this.memoryStore.setMemoryPinned(selector, value, pinned);
1943
+ if (!updated) {
1944
+ await this.sendTextReply(context.chat_id, `未找到可更新的${target.label} ID: ${value}`, context.message_id, context.text);
1945
+ return;
1946
+ }
1947
+ await this.auditLog.append({
1948
+ type: pinned ? 'memory.pin' : 'memory.unpin',
1949
+ chat_id: context.chat_id,
1950
+ actor_id: context.actor_id,
1951
+ project_alias: projectContext.projectAlias,
1952
+ scope: target.scope,
1953
+ memory_id: value,
1954
+ });
1955
+ await this.sendTextReply(context.chat_id, [
1956
+ `${target.label}${pinned ? '已置顶' : '已取消置顶'}: ${updated.title}`,
1957
+ `memory_id: ${updated.id}`,
1958
+ ...(agedOutMemoryId ? [`已自动老化旧置顶: ${agedOutMemoryTitle} (${agedOutMemoryId})`] : []),
1959
+ ].join('\n'), context.message_id, context.text);
1960
+ return;
1961
+ }
1962
+ const content = value?.trim();
1963
+ if (!content) {
1964
+ await this.sendTextReply(context.chat_id, '用法: /memory save <text> 或 /memory save group <text>', context.message_id, context.text);
1965
+ return;
1966
+ }
1967
+ const target = this.resolveMemoryTarget(context, scope);
1968
+ const title = truncateExcerpt(content.replace(/\s+/g, ' ').trim(), 60);
1969
+ const expiresAt = this.buildMemoryExpiresAt();
1970
+ const saved = await this.memoryStore.saveMemory({
1971
+ scope: target.scope,
1972
+ project_alias: projectContext.projectAlias,
1973
+ chat_id: target.chatId,
1974
+ title,
1975
+ content,
1976
+ tags: filters?.tag ? [filters.tag] : undefined,
1977
+ source: filters?.source ?? 'manual',
1978
+ created_by: context.actor_id,
1979
+ expires_at: expiresAt,
1980
+ });
1981
+ await this.auditLog.append({
1982
+ type: 'memory.save',
1983
+ chat_id: context.chat_id,
1984
+ actor_id: context.actor_id,
1985
+ project_alias: projectContext.projectAlias,
1986
+ scope: target.scope,
1987
+ memory_id: saved.id,
1988
+ title: saved.title,
1989
+ });
1990
+ await this.sendTextReply(context.chat_id, [
1991
+ `项目: ${projectContext.projectAlias}`,
1992
+ `已保存${target.label}: ${saved.title}`,
1993
+ `memory_id: ${saved.id}`,
1994
+ ...(saved.expires_at ? [`expires_at: ${saved.expires_at}`] : []),
1995
+ ].join('\n'), context.message_id, context.text);
1996
+ }
1997
+ catch (error) {
1998
+ const message = error instanceof Error ? error.message : String(error);
1999
+ await this.sendTextReply(context.chat_id, message, context.message_id, context.text);
2000
+ }
2001
+ }
2002
+ renderMemoryFilterLines(filters) {
2003
+ return [
2004
+ ...(filters?.tag ? [`tag: ${filters.tag}`] : []),
2005
+ ...(filters?.source ? [`source: ${filters.source}`] : []),
2006
+ ...(filters?.created_by ? [`created_by: ${filters.created_by}`] : []),
2007
+ ];
2008
+ }
2009
+ async handleKnowledgeCommand(context, selectionKey, action, query) {
2010
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
2011
+ const roots = await resolveKnowledgeRoots(projectContext.project);
2012
+ if (action === 'status') {
2013
+ const message = roots.length
2014
+ ? [`项目: ${projectContext.projectAlias}`, '知识库目录:', ...roots.map((root) => `- ${root}`)].join('\n')
2015
+ : [`项目: ${projectContext.projectAlias}`, '当前没有可用知识库目录。', '可在项目配置中设置 knowledge_paths,或在项目根下提供 docs/README。'].join('\n');
2016
+ await this.sendTextReply(context.chat_id, message, context.message_id, context.text);
2017
+ return;
2018
+ }
2019
+ if (!query) {
2020
+ await this.sendTextReply(context.chat_id, '用法: /kb search <query>', context.message_id, context.text);
2021
+ return;
2022
+ }
2023
+ const result = await searchKnowledgeBase(projectContext.project, query, 5);
2024
+ await this.auditLog.append({
2025
+ type: 'knowledge.search',
2026
+ chat_id: context.chat_id,
2027
+ actor_id: context.actor_id,
2028
+ project_alias: projectContext.projectAlias,
2029
+ query,
2030
+ result_count: result.matches.length,
2031
+ });
2032
+ if (result.roots.length === 0) {
2033
+ await this.sendTextReply(context.chat_id, [`项目: ${projectContext.projectAlias}`, '当前没有可搜索的知识库目录。', '可在项目配置中设置 knowledge_paths,或在项目根下提供 docs/README。'].join('\n'), context.message_id, context.text);
2034
+ return;
2035
+ }
2036
+ if (result.matches.length === 0) {
2037
+ await this.sendTextReply(context.chat_id, [`项目: ${projectContext.projectAlias}`, `知识库搜索: ${query}`, '未找到匹配项。', '', '搜索目录:', ...result.roots.map((root) => `- ${root}`)].join('\n'), context.message_id, context.text);
2038
+ return;
2039
+ }
2040
+ const lines = result.matches.map((match, index) => {
2041
+ const relativePath = match.file.startsWith(projectContext.project.root)
2042
+ ? match.file.slice(projectContext.project.root.length + 1)
2043
+ : match.file;
2044
+ return `${index + 1}. ${relativePath}:${match.line}\n ${truncateExcerpt(match.text, 140)}`;
2045
+ });
2046
+ await this.sendTextReply(context.chat_id, [`项目: ${projectContext.projectAlias}`, `知识库搜索: ${query}`, '', ...lines].join('\n'), context.message_id, context.text);
2047
+ }
2048
+ async handleDocCommand(context, selectionKey, action, value, extra) {
2049
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
2050
+ if (!canAccessProject(this.config, projectContext.projectAlias, context.chat_id, action === 'create' ? 'operator' : 'viewer')) {
2051
+ await this.sendTextReply(context.chat_id, `当前 chat_id 无权${action === 'create' ? '写入' : '读取'}项目 ${projectContext.projectAlias} 关联的飞书文档。`, context.message_id, context.text);
2052
+ return;
2053
+ }
2054
+ const docClient = new FeishuDocClient(this.feishuClient.createSdkClient());
2055
+ if (action === 'create') {
2056
+ const title = value?.trim();
2057
+ if (!title) {
2058
+ await this.sendTextReply(context.chat_id, '用法: /doc create <title>', context.message_id, context.text);
2059
+ return;
2060
+ }
2061
+ const created = await docClient.create(title, extra?.trim());
2062
+ await this.auditLog.append({
2063
+ type: 'doc.create',
2064
+ chat_id: context.chat_id,
2065
+ actor_id: context.actor_id,
2066
+ project_alias: projectContext.projectAlias,
2067
+ document_id: created.documentId,
2068
+ title: created.title,
2069
+ });
2070
+ await this.sendTextReply(context.chat_id, ['已创建飞书文档', `标题: ${created.title ?? title}`, `文档: ${created.documentId}`, ...(created.url ? [`链接: ${created.url}`] : [])].join('\n'), context.message_id, context.text);
2071
+ return;
2072
+ }
2073
+ if (!value) {
2074
+ await this.sendTextReply(context.chat_id, '用法: /doc read <url|token>', context.message_id, context.text);
2075
+ return;
2076
+ }
2077
+ const document = await docClient.read(value);
2078
+ await this.auditLog.append({
2079
+ type: 'doc.read',
2080
+ chat_id: context.chat_id,
2081
+ actor_id: context.actor_id,
2082
+ project_alias: projectContext.projectAlias,
2083
+ document_id: document.documentId,
2084
+ title: document.title,
2085
+ });
2086
+ await this.sendTextReply(context.chat_id, [
2087
+ `标题: ${document.title ?? '未知'}`,
2088
+ `文档: ${document.documentId}`,
2089
+ ...(document.url ? [`链接: ${document.url}`] : []),
2090
+ '',
2091
+ truncateExcerpt(document.content?.replace(/\s+/g, ' ').trim() ?? '文档暂无可读取的纯文本内容。', 1200),
2092
+ ].join('\n'), context.message_id, context.text);
2093
+ }
2094
+ async handleTaskCommand(context, selectionKey, action, value) {
2095
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
2096
+ if (!canAccessProject(this.config, projectContext.projectAlias, context.chat_id, action === 'create' || action === 'complete' ? 'operator' : 'viewer')) {
2097
+ await this.sendTextReply(context.chat_id, `当前 chat_id 无权${action === 'create' || action === 'complete' ? '写入' : '查看'}项目 ${projectContext.projectAlias} 关联的飞书任务。`, context.message_id, context.text);
2098
+ return;
2099
+ }
2100
+ const taskClient = new FeishuTaskClient(this.feishuClient.createSdkClient());
2101
+ if (action === 'list') {
2102
+ const limit = clampListLimit(value, 10, 20);
2103
+ const tasks = await taskClient.list(limit);
2104
+ const lines = tasks.length > 0
2105
+ ? tasks.map((task, index) => `${index + 1}. ${task.summary ?? '(无标题)'}\n guid: ${task.guid}\n status: ${task.status ?? 'unknown'}${task.url ? `\n url: ${task.url}` : ''}`)
2106
+ : ['当前没有可见任务。'];
2107
+ await this.sendTextReply(context.chat_id, ['最近任务', '', ...lines].join('\n'), context.message_id, context.text);
2108
+ return;
2109
+ }
2110
+ if (action === 'get') {
2111
+ if (!value) {
2112
+ await this.sendTextReply(context.chat_id, '用法: /task get <task_guid>', context.message_id, context.text);
2113
+ return;
2114
+ }
2115
+ const task = await taskClient.get(value);
2116
+ await this.auditLog.append({
2117
+ type: 'task.read',
2118
+ chat_id: context.chat_id,
2119
+ actor_id: context.actor_id,
2120
+ project_alias: projectContext.projectAlias,
2121
+ task_guid: task.guid,
2122
+ });
2123
+ await this.sendTextReply(context.chat_id, [
2124
+ `任务: ${task.summary ?? '(无标题)'}`,
2125
+ `guid: ${task.guid}`,
2126
+ `status: ${task.status ?? 'unknown'}`,
2127
+ ...(task.url ? [`链接: ${task.url}`] : []),
2128
+ '',
2129
+ task.description ?? '无描述',
2130
+ ].join('\n'), context.message_id, context.text);
2131
+ return;
2132
+ }
2133
+ if (action === 'create') {
2134
+ const summary = value?.trim();
2135
+ if (!summary) {
2136
+ await this.sendTextReply(context.chat_id, '用法: /task create <summary>', context.message_id, context.text);
2137
+ return;
2138
+ }
2139
+ const task = await taskClient.create(summary);
2140
+ await this.auditLog.append({
2141
+ type: 'task.create',
2142
+ chat_id: context.chat_id,
2143
+ actor_id: context.actor_id,
2144
+ project_alias: projectContext.projectAlias,
2145
+ task_guid: task.guid,
2146
+ summary: task.summary,
2147
+ });
2148
+ await this.sendTextReply(context.chat_id, [`已创建任务`, `标题: ${task.summary ?? summary}`, `guid: ${task.guid}`, ...(task.url ? [`链接: ${task.url}`] : [])].join('\n'), context.message_id, context.text);
2149
+ return;
2150
+ }
2151
+ if (!value) {
2152
+ await this.sendTextReply(context.chat_id, '用法: /task complete <task_guid>', context.message_id, context.text);
2153
+ return;
2154
+ }
2155
+ const task = await taskClient.complete(value);
2156
+ await this.auditLog.append({
2157
+ type: 'task.complete',
2158
+ chat_id: context.chat_id,
2159
+ actor_id: context.actor_id,
2160
+ project_alias: projectContext.projectAlias,
2161
+ task_guid: task.guid,
2162
+ summary: task.summary,
2163
+ });
2164
+ await this.sendTextReply(context.chat_id, [`已完成任务`, `标题: ${task.summary ?? '(无标题)'}`, `guid: ${task.guid}`, `status: ${task.status ?? 'unknown'}`].join('\n'), context.message_id, context.text);
2165
+ }
2166
+ async handleBaseCommand(context, selectionKey, action, appToken, tableId, recordId, value) {
2167
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
2168
+ if (!canAccessProject(this.config, projectContext.projectAlias, context.chat_id, action === 'create' || action === 'update' ? 'operator' : 'viewer')) {
2169
+ await this.sendTextReply(context.chat_id, `当前 chat_id 无权${action === 'create' || action === 'update' ? '写入' : '查看'}项目 ${projectContext.projectAlias} 关联的多维表格。`, context.message_id, context.text);
2170
+ return;
2171
+ }
2172
+ const baseClient = new FeishuBaseClient(this.feishuClient.createSdkClient());
2173
+ if (action === 'tables') {
2174
+ if (!appToken) {
2175
+ await this.sendTextReply(context.chat_id, '用法: /base tables <app_token>', context.message_id, context.text);
2176
+ return;
2177
+ }
2178
+ const tables = await baseClient.listTables(appToken, 20);
2179
+ const lines = tables.length > 0
2180
+ ? tables.map((table, index) => `${index + 1}. ${table.name ?? '(未命名表)'}\n table_id: ${table.tableId}${table.revision !== undefined ? `\n revision: ${table.revision}` : ''}`)
2181
+ : ['当前 Base 中没有可见数据表。'];
2182
+ await this.sendTextReply(context.chat_id, [`Base: ${appToken}`, '', ...lines].join('\n'), context.message_id, context.text);
2183
+ return;
2184
+ }
2185
+ if (action === 'records') {
2186
+ if (!appToken || !tableId) {
2187
+ await this.sendTextReply(context.chat_id, '用法: /base records <app_token> <table_id> [limit]', context.message_id, context.text);
2188
+ return;
2189
+ }
2190
+ const limit = clampListLimit(value, 10, 20);
2191
+ const records = await baseClient.listRecords(appToken, tableId, limit);
2192
+ const lines = records.length > 0
2193
+ ? records.map((record, index) => `${index + 1}. ${record.recordId}\n fields: ${truncateExcerpt(JSON.stringify(record.fields), 240)}${record.recordUrl ? `\n url: ${record.recordUrl}` : ''}`)
2194
+ : ['当前数据表没有可见记录。'];
2195
+ await this.sendTextReply(context.chat_id, [`Base: ${appToken}`, `Table: ${tableId}`, '', ...lines].join('\n'), context.message_id, context.text);
2196
+ return;
2197
+ }
2198
+ if (action === 'create') {
2199
+ if (!appToken || !tableId || !value) {
2200
+ await this.sendTextReply(context.chat_id, '用法: /base create <app_token> <table_id> <json>', context.message_id, context.text);
2201
+ return;
2202
+ }
2203
+ const fields = parseJsonObject(value);
2204
+ const record = await baseClient.createRecord(appToken, tableId, fields);
2205
+ await this.auditLog.append({
2206
+ type: 'base.record.create',
2207
+ chat_id: context.chat_id,
2208
+ actor_id: context.actor_id,
2209
+ project_alias: projectContext.projectAlias,
2210
+ app_token: appToken,
2211
+ table_id: tableId,
2212
+ record_id: record.recordId,
2213
+ });
2214
+ await this.sendTextReply(context.chat_id, [`已创建 Base 记录`, `app: ${appToken}`, `table: ${tableId}`, `record: ${record.recordId}`, `fields: ${truncateExcerpt(JSON.stringify(record.fields), 240)}`].join('\n'), context.message_id, context.text);
2215
+ return;
2216
+ }
2217
+ if (!appToken || !tableId || !recordId || !value) {
2218
+ await this.sendTextReply(context.chat_id, '用法: /base update <app_token> <table_id> <record_id> <json>', context.message_id, context.text);
2219
+ return;
2220
+ }
2221
+ const fields = parseJsonObject(value);
2222
+ const record = await baseClient.updateRecord(appToken, tableId, recordId, fields);
2223
+ await this.auditLog.append({
2224
+ type: 'base.record.update',
2225
+ chat_id: context.chat_id,
2226
+ actor_id: context.actor_id,
2227
+ project_alias: projectContext.projectAlias,
2228
+ app_token: appToken,
2229
+ table_id: tableId,
2230
+ record_id: record.recordId,
2231
+ });
2232
+ await this.sendTextReply(context.chat_id, [`已更新 Base 记录`, `app: ${appToken}`, `table: ${tableId}`, `record: ${record.recordId}`, `fields: ${truncateExcerpt(JSON.stringify(record.fields), 240)}`].join('\n'), context.message_id, context.text);
2233
+ }
2234
+ async handleWikiCommand(context, selectionKey, action, value, extra, target, role) {
2235
+ const projectContext = await this.resolveProjectContext(context, selectionKey);
2236
+ const wikiClient = new FeishuWikiClient(this.feishuClient.createSdkClient());
2237
+ if (action === 'spaces') {
2238
+ const spaces = await wikiClient.listSpaces(10);
2239
+ const lines = spaces.length > 0
2240
+ ? spaces.map((space) => `- ${space.name} (${space.id})${space.description ? ` | ${space.description}` : ''}`)
2241
+ : ['当前应用可访问的知识空间为空。请确认机器人已被加入目标空间。'];
2242
+ await this.sendTextReply(context.chat_id, [`项目: ${projectContext.projectAlias}`, `配置过滤空间数: ${projectContext.project.wiki_space_ids.length}`, '', ...lines].join('\n'), context.message_id, context.text);
2243
+ return;
2244
+ }
2245
+ if (action === 'search') {
2246
+ if (!value) {
2247
+ await this.sendTextReply(context.chat_id, '用法: /wiki search <query>', context.message_id, context.text);
2248
+ return;
2249
+ }
2250
+ const hits = await wikiClient.search(value, projectContext.project.wiki_space_ids, 5);
2251
+ await this.auditLog.append({
2252
+ type: 'wiki.search',
2253
+ chat_id: context.chat_id,
2254
+ actor_id: context.actor_id,
2255
+ project_alias: projectContext.projectAlias,
2256
+ query: value,
2257
+ result_count: hits.length,
2258
+ });
2259
+ if (hits.length === 0) {
2260
+ await this.sendTextReply(context.chat_id, [`项目: ${projectContext.projectAlias}`, `飞书知识库搜索: ${value}`, '未找到匹配结果。', '', '提示: 确认机器人有目标空间访问权限,或在项目配置里设置 wiki_space_ids。'].join('\n'), context.message_id, context.text);
2261
+ return;
2262
+ }
2263
+ const lines = hits.map((hit, index) => [
2264
+ `${index + 1}. ${hit.title}`,
2265
+ ` space: ${hit.spaceId}`,
2266
+ ` token: ${hit.objToken}`,
2267
+ ...(hit.url ? [` url: ${hit.url}`] : []),
2268
+ ].join('\n'));
2269
+ await this.sendTextReply(context.chat_id, [`项目: ${projectContext.projectAlias}`, `飞书知识库搜索: ${value}`, '', ...lines].join('\n'), context.message_id, context.text);
2270
+ return;
2271
+ }
2272
+ if (action === 'members') {
2273
+ const spaceId = value?.trim() || projectContext.project.wiki_space_ids[0];
2274
+ if (!spaceId) {
2275
+ await this.sendTextReply(context.chat_id, '用法: /wiki members [space_id],或先在项目配置里设置默认 wiki_space_ids。', context.message_id, context.text);
2276
+ return;
2277
+ }
2278
+ const members = await wikiClient.listMembers(spaceId, 20);
2279
+ await this.auditLog.append({
2280
+ type: 'wiki.members',
2281
+ chat_id: context.chat_id,
2282
+ actor_id: context.actor_id,
2283
+ project_alias: projectContext.projectAlias,
2284
+ space_id: spaceId,
2285
+ result_count: members.length,
2286
+ });
2287
+ const lines = members.length > 0
2288
+ ? members.map((member, index) => `${index + 1}. ${member.memberId}\n member_type: ${member.memberType}\n role: ${member.memberRole}${member.type ? `\n type: ${member.type}` : ''}`)
2289
+ : ['当前知识空间没有可见成员,或机器人没有成员读取权限。'];
2290
+ await this.sendTextReply(context.chat_id, [`项目: ${projectContext.projectAlias}`, `知识空间成员: ${spaceId}`, '', ...lines].join('\n'), context.message_id, context.text);
2291
+ return;
2292
+ }
2293
+ if (action === 'create') {
2294
+ const defaultSpaceId = projectContext.project.wiki_space_ids[0];
2295
+ const spaceId = extra ?? defaultSpaceId;
2296
+ const title = value?.trim();
2297
+ if (!title) {
2298
+ await this.sendTextReply(context.chat_id, '用法: /wiki create <title> 或 /wiki create <space_id> <title>', context.message_id, context.text);
2299
+ return;
2300
+ }
2301
+ if (!spaceId) {
2302
+ await this.sendTextReply(context.chat_id, '当前项目未配置默认 wiki_space_ids,请使用 `/wiki create <space_id> <title>`。', context.message_id, context.text);
2303
+ return;
2304
+ }
2305
+ const created = await wikiClient.createDoc(spaceId, title);
2306
+ await this.auditLog.append({
2307
+ type: 'wiki.create',
2308
+ chat_id: context.chat_id,
2309
+ actor_id: context.actor_id,
2310
+ project_alias: projectContext.projectAlias,
2311
+ title,
2312
+ space_id: created.spaceId,
2313
+ obj_token: created.objToken,
2314
+ node_token: created.nodeToken,
2315
+ });
2316
+ await this.sendTextReply(context.chat_id, [
2317
+ `项目: ${projectContext.projectAlias}`,
2318
+ `已创建飞书文档: ${created.title ?? title}`,
2319
+ `空间: ${created.spaceId ?? spaceId}`,
2320
+ ...(created.nodeToken ? [`节点: ${created.nodeToken}`] : []),
2321
+ ...(created.objToken ? [`文档: ${created.objToken}`] : []),
2322
+ ].join('\n'), context.message_id, context.text);
2323
+ return;
2324
+ }
2325
+ if (action === 'grant') {
2326
+ const spaceId = extra?.trim();
2327
+ const memberType = target?.trim();
2328
+ const memberId = value?.trim();
2329
+ const memberRole = role?.trim() || 'member';
2330
+ if (!spaceId || !memberType || !memberId) {
2331
+ await this.sendTextReply(context.chat_id, '用法: /wiki grant <space_id> <member_type> <member_id> [member|admin]', context.message_id, context.text);
2332
+ return;
2333
+ }
2334
+ const granted = await wikiClient.addMember(spaceId, memberType, memberId, memberRole);
2335
+ await this.auditLog.append({
2336
+ type: 'wiki.member.grant',
2337
+ chat_id: context.chat_id,
2338
+ actor_id: context.actor_id,
2339
+ project_alias: projectContext.projectAlias,
2340
+ space_id: spaceId,
2341
+ member_id: granted.memberId,
2342
+ member_type: granted.memberType,
2343
+ member_role: granted.memberRole,
2344
+ });
2345
+ await this.sendTextReply(context.chat_id, [
2346
+ `项目: ${projectContext.projectAlias}`,
2347
+ '已添加知识空间成员',
2348
+ `空间: ${spaceId}`,
2349
+ `member_type: ${granted.memberType}`,
2350
+ `member_id: ${granted.memberId}`,
2351
+ `role: ${granted.memberRole}`,
2352
+ ].join('\n'), context.message_id, context.text);
2353
+ return;
2354
+ }
2355
+ if (action === 'rename') {
2356
+ const nodeToken = extra?.trim();
2357
+ const title = value?.trim();
2358
+ if (!nodeToken || !title) {
2359
+ await this.sendTextReply(context.chat_id, '用法: /wiki rename <node_token> <title>', context.message_id, context.text);
2360
+ return;
2361
+ }
2362
+ await wikiClient.renameNode(nodeToken, title, projectContext.project.wiki_space_ids[0]);
2363
+ await this.auditLog.append({
2364
+ type: 'wiki.rename',
2365
+ chat_id: context.chat_id,
2366
+ actor_id: context.actor_id,
2367
+ project_alias: projectContext.projectAlias,
2368
+ node_token: nodeToken,
2369
+ title,
2370
+ });
2371
+ await this.sendTextReply(context.chat_id, [`项目: ${projectContext.projectAlias}`, `已更新知识库节点标题`, `节点: ${nodeToken}`, `标题: ${title}`].join('\n'), context.message_id, context.text);
2372
+ return;
2373
+ }
2374
+ if (action === 'copy') {
2375
+ const nodeToken = value?.trim();
2376
+ const targetSpaceId = extra?.trim() || projectContext.project.wiki_space_ids[0];
2377
+ if (!nodeToken) {
2378
+ await this.sendTextReply(context.chat_id, '用法: /wiki copy <node_token> [target_space_id]', context.message_id, context.text);
2379
+ return;
2380
+ }
2381
+ if (!targetSpaceId) {
2382
+ await this.sendTextReply(context.chat_id, '当前项目未配置默认 wiki_space_ids,请显式传入 target_space_id。', context.message_id, context.text);
2383
+ return;
2384
+ }
2385
+ const copied = await wikiClient.copyNode(nodeToken, targetSpaceId);
2386
+ await this.auditLog.append({
2387
+ type: 'wiki.copy',
2388
+ chat_id: context.chat_id,
2389
+ actor_id: context.actor_id,
2390
+ project_alias: projectContext.projectAlias,
2391
+ node_token: nodeToken,
2392
+ target_space_id: copied.spaceId,
2393
+ obj_token: copied.objToken,
2394
+ });
2395
+ await this.sendTextReply(context.chat_id, [
2396
+ `项目: ${projectContext.projectAlias}`,
2397
+ `已复制知识库节点`,
2398
+ `源节点: ${nodeToken}`,
2399
+ `目标空间: ${copied.spaceId ?? targetSpaceId}`,
2400
+ ...(copied.nodeToken ? [`新节点: ${copied.nodeToken}`] : []),
2401
+ ...(copied.objToken ? [`对象: ${copied.objToken}`] : []),
2402
+ ].join('\n'), context.message_id, context.text);
2403
+ return;
2404
+ }
2405
+ if (action === 'move') {
2406
+ const sourceSpaceId = extra?.trim();
2407
+ const nodeToken = value?.trim();
2408
+ const targetSpaceId = target?.trim() || projectContext.project.wiki_space_ids[0];
2409
+ if (!sourceSpaceId || !nodeToken) {
2410
+ await this.sendTextReply(context.chat_id, '用法: /wiki move <source_space_id> <node_token> [target_space_id]', context.message_id, context.text);
2411
+ return;
2412
+ }
2413
+ if (!targetSpaceId) {
2414
+ await this.sendTextReply(context.chat_id, '当前项目未配置默认 wiki_space_ids,请显式传入 target_space_id。', context.message_id, context.text);
2415
+ return;
2416
+ }
2417
+ const moved = await wikiClient.moveNode(sourceSpaceId, nodeToken, targetSpaceId);
2418
+ await this.auditLog.append({
2419
+ type: 'wiki.move',
2420
+ chat_id: context.chat_id,
2421
+ actor_id: context.actor_id,
2422
+ project_alias: projectContext.projectAlias,
2423
+ node_token: nodeToken,
2424
+ source_space_id: sourceSpaceId,
2425
+ target_space_id: moved.spaceId,
2426
+ obj_token: moved.objToken,
2427
+ });
2428
+ await this.sendTextReply(context.chat_id, [
2429
+ `项目: ${projectContext.projectAlias}`,
2430
+ `已移动知识库节点`,
2431
+ `源空间: ${sourceSpaceId}`,
2432
+ `源节点: ${nodeToken}`,
2433
+ `目标空间: ${moved.spaceId ?? targetSpaceId}`,
2434
+ ...(moved.nodeToken ? [`当前节点: ${moved.nodeToken}`] : []),
2435
+ ].join('\n'), context.message_id, context.text);
2436
+ return;
2437
+ }
2438
+ if (action === 'revoke') {
2439
+ const spaceId = extra?.trim();
2440
+ const memberType = target?.trim();
2441
+ const memberId = value?.trim();
2442
+ const memberRole = role?.trim() || 'member';
2443
+ if (!spaceId || !memberType || !memberId) {
2444
+ await this.sendTextReply(context.chat_id, '用法: /wiki revoke <space_id> <member_type> <member_id> [member|admin]', context.message_id, context.text);
2445
+ return;
2446
+ }
2447
+ const revoked = await wikiClient.removeMember(spaceId, memberType, memberId, memberRole);
2448
+ await this.auditLog.append({
2449
+ type: 'wiki.member.revoke',
2450
+ chat_id: context.chat_id,
2451
+ actor_id: context.actor_id,
2452
+ project_alias: projectContext.projectAlias,
2453
+ space_id: spaceId,
2454
+ member_id: revoked.memberId,
2455
+ member_type: revoked.memberType,
2456
+ member_role: revoked.memberRole,
2457
+ });
2458
+ await this.sendTextReply(context.chat_id, [
2459
+ `项目: ${projectContext.projectAlias}`,
2460
+ '已移除知识空间成员',
2461
+ `空间: ${spaceId}`,
2462
+ `member_type: ${revoked.memberType}`,
2463
+ `member_id: ${revoked.memberId}`,
2464
+ `role: ${revoked.memberRole}`,
2465
+ ].join('\n'), context.message_id, context.text);
2466
+ return;
2467
+ }
2468
+ if (!value) {
2469
+ await this.sendTextReply(context.chat_id, '用法: /wiki read <url|token>', context.message_id, context.text);
2470
+ return;
2471
+ }
2472
+ const result = await wikiClient.read(value);
2473
+ await this.auditLog.append({
2474
+ type: 'wiki.read',
2475
+ chat_id: context.chat_id,
2476
+ actor_id: context.actor_id,
2477
+ project_alias: projectContext.projectAlias,
2478
+ target: value,
2479
+ obj_type: result.objType,
2480
+ obj_token: result.objToken,
2481
+ });
2482
+ const summary = result.content ? truncateExcerpt(result.content.replace(/\s+/g, ' ').trim(), 1200) : '当前对象不是 docx 文档,暂不支持直接拉取纯文本内容。';
2483
+ await this.sendTextReply(context.chat_id, [
2484
+ `项目: ${projectContext.projectAlias}`,
2485
+ `标题: ${result.title ?? '未知'}`,
2486
+ `类型: ${result.objType ?? '未知'}`,
2487
+ ...(result.spaceId ? [`空间: ${result.spaceId}`] : []),
2488
+ ...(result.objToken ? [`对象: ${result.objToken}`] : []),
2489
+ ...(result.url ? [`链接: ${result.url}`] : []),
2490
+ '',
2491
+ summary,
2492
+ ].join('\n'), context.message_id, context.text);
2493
+ }
2494
+ async buildProjectsText(selectionKey, chatId) {
2495
+ const selected = await this.resolveProjectAlias(selectionKey);
2496
+ const visibleAliases = chatId ? filterAccessibleProjects(this.config, chatId) : Object.keys(this.config.projects);
2497
+ const lines = Object.entries(this.config.projects)
2498
+ .filter(([alias]) => visibleAliases.includes(alias))
2499
+ .map(([alias, project]) => {
2500
+ const marker = alias === selected ? '*' : '-';
2501
+ const description = project.description ? ` | ${project.description}` : '';
2502
+ return `${marker} ${alias}: ${project.root}${description}`;
2503
+ });
2504
+ if (chatId && lines.length === 0) {
2505
+ return '当前 chat_id 没有任何可访问项目。请联系管理员分配 viewer/operator/admin 权限。';
2506
+ }
2507
+ return ['可用项目:', ...(lines.length > 0 ? lines : ['(empty)'])].join('\n');
2508
+ }
2509
+ async buildStatusText(projectAlias, conversation, activeRun) {
2510
+ const session = conversation?.projects[projectAlias];
2511
+ const sessions = conversation ? await this.sessionStore.listProjectSessions(buildConversationKeyForConversation(conversation), projectAlias) : [];
2512
+ const memoryCount = this.config.service.memory_enabled ? await this.memoryStore.countProjectMemories(projectAlias) : 0;
2513
+ const threadSummary = this.config.service.memory_enabled && conversation && session?.thread_id
2514
+ ? await this.memoryStore.getThreadSummary(buildConversationKeyForConversation(conversation), projectAlias, session.thread_id)
2515
+ : null;
2516
+ return [
2517
+ `项目: ${projectAlias}`,
2518
+ `当前会话: ${session?.thread_id ?? activeRun?.session_id ?? '未开始'}`,
2519
+ `已保存会话数: ${sessions.length}`,
2520
+ `项目记忆数: ${memoryCount}`,
2521
+ `最近更新时间: ${session?.updated_at ?? conversation?.updated_at ?? activeRun?.updated_at ?? '无'}`,
2522
+ `当前运行状态: ${activeRun?.status ?? '无'}`,
2523
+ ...(activeRun?.status === 'queued' && activeRun.status_detail ? ['', activeRun.status_detail] : []),
2524
+ '',
2525
+ threadSummary?.summary ?? session?.last_response_excerpt ?? '暂无回复摘要。',
2526
+ ].join('\n');
2527
+ }
2528
+ async buildDetailedStatusText(projectAlias, sessionKey, conversation, activeRun) {
2529
+ const session = conversation?.projects[projectAlias];
2530
+ const runs = await this.runStateStore.listRuns();
2531
+ const currentProjectRuns = runs.filter((run) => run.queue_key === buildQueueKey(sessionKey, projectAlias));
2532
+ const recentFailure = currentProjectRuns.find((run) => run.status === 'failure' || run.status === 'cancelled' || run.status === 'stale');
2533
+ const lines = [
2534
+ await this.buildStatusText(projectAlias, conversation, activeRun),
2535
+ '',
2536
+ '详细状态',
2537
+ `当前队列耗时: ${activeRun ? formatAgeFromNow(activeRun.started_at) : '无'}`,
2538
+ `当前运行更新时间: ${activeRun ? formatAgeFromNow(activeRun.updated_at) : '无'}`,
2539
+ activeRun?.project_root ? `项目根: ${activeRun.project_root}` : null,
2540
+ activeRun?.prompt_excerpt ? `当前提示摘要: ${activeRun.prompt_excerpt}` : null,
2541
+ recentFailure ? '' : null,
2542
+ recentFailure ? '最近失败' : null,
2543
+ recentFailure ? `状态: ${recentFailure.status}` : null,
2544
+ recentFailure?.error ? `原因: ${recentFailure.error}` : null,
2545
+ recentFailure ? `发生时间: ${recentFailure.updated_at}` : null,
2546
+ session?.last_prompt ? '' : null,
2547
+ session?.last_prompt ? `最近提示词: ${truncateExcerpt(session.last_prompt, 120)}` : null,
2548
+ ];
2549
+ return lines.filter(Boolean).join('\n');
2550
+ }
2551
+ buildStatusCardFromConversation(projectAlias, sessionKey, conversation, activeRun, fallbackChatId) {
2552
+ const session = conversation?.projects[projectAlias];
2553
+ const sessionCount = Object.keys(session?.sessions ?? {}).length;
2554
+ const isExecutableRun = activeRun ? isExecutionRunStatus(activeRun.status) : false;
2555
+ const includeActions = this.supportsInteractiveCardActions();
2556
+ const actionChatId = conversation?.chat_id ?? activeRun?.chat_id ?? fallbackChatId;
2557
+ return buildStatusCard({
2558
+ title: '当前会话状态',
2559
+ summary: this.buildRunStatusSummary(session?.last_response_excerpt, activeRun),
2560
+ projectAlias,
2561
+ sessionId: session?.thread_id,
2562
+ runStatus: activeRun?.status,
2563
+ runPhase: activeRun ? mapRunStatusToPhase(activeRun.status) : undefined,
2564
+ sessionCount,
2565
+ includeActions,
2566
+ rerunPayload: includeActions && session?.last_prompt && !activeRun
2567
+ ? {
2568
+ action: 'rerun',
2569
+ conversation_key: sessionKey,
2570
+ project_alias: projectAlias,
2571
+ chat_id: actionChatId ?? '',
2572
+ }
2573
+ : undefined,
2574
+ newSessionPayload: includeActions
2575
+ ? {
2576
+ action: 'new',
2577
+ conversation_key: sessionKey,
2578
+ project_alias: projectAlias,
2579
+ chat_id: actionChatId ?? '',
2580
+ }
2581
+ : undefined,
2582
+ cancelPayload: includeActions && isExecutableRun
2583
+ ? {
2584
+ action: 'cancel',
2585
+ conversation_key: sessionKey,
2586
+ project_alias: projectAlias,
2587
+ chat_id: actionChatId ?? '',
2588
+ }
2589
+ : undefined,
2590
+ statusPayload: includeActions
2591
+ ? {
2592
+ action: 'status',
2593
+ conversation_key: sessionKey,
2594
+ project_alias: projectAlias,
2595
+ chat_id: actionChatId ?? '',
2596
+ }
2597
+ : undefined,
2598
+ });
2599
+ }
2600
+ async buildBridgePrompt(projectAlias, project, incomingMessage, userPrompt, memoryContext) {
2601
+ const prefixParts = [
2602
+ 'You are replying through Feique, a team AI collaboration hub connected via Feishu.',
2603
+ 'Keep the final response concise and action-oriented.',
2604
+ 'When files change, summarize key paths and verification.',
2605
+ 'Do not expose session IDs, run IDs, chat IDs, conversation keys, secrets, raw logs, or absolute local filesystem paths to Feishu users unless they explicitly ask for them.',
2606
+ 'Prefer project-relative paths over absolute paths when referencing files.',
2607
+ this.config.codex.bridge_instructions,
2608
+ ].filter(Boolean);
2609
+ if (project.instructions_prefix) {
2610
+ try {
2611
+ const projectInstructions = (await fs.readFile(project.instructions_prefix, 'utf8')).trim();
2612
+ if (projectInstructions) {
2613
+ prefixParts.push(projectInstructions);
2614
+ }
2615
+ }
2616
+ catch (error) {
2617
+ this.logger.warn({ error, projectAlias }, 'Failed to read project instructions prefix');
2618
+ }
2619
+ }
2620
+ return [
2621
+ ...prefixParts,
2622
+ '',
2623
+ `Current project alias: ${projectAlias}`,
2624
+ `Current project root: ${project.root}`,
2625
+ `Feishu message type: ${incomingMessage.message_type}`,
2626
+ ...(memoryContext.threadSummary?.summary
2627
+ ? [
2628
+ '',
2629
+ 'Thread summary:',
2630
+ truncateExcerpt(memoryContext.threadSummary.summary, this.config.service.memory_prompt_max_chars),
2631
+ ]
2632
+ : []),
2633
+ ...renderMemorySection('Project memory', [...memoryContext.pinnedMemories, ...memoryContext.relevantMemories], this.config.service.memory_prompt_max_chars),
2634
+ ...renderMemorySection('Group shared memory', [...memoryContext.pinnedGroupMemories, ...memoryContext.relevantGroupMemories], this.config.service.memory_prompt_max_chars),
2635
+ '',
2636
+ 'User message from Feishu:',
2637
+ userPrompt || '[no text body]',
2638
+ ...(incomingMessage.attachments.length > 0
2639
+ ? [
2640
+ '',
2641
+ 'Message attachments:',
2642
+ ...incomingMessage.attachments.map((attachment, index) => [
2643
+ `${index + 1}. ${attachment.summary}`,
2644
+ ...(attachment.downloaded_path ? [` downloaded_path: ${attachment.downloaded_path}`] : []),
2645
+ ...(attachment.content_excerpt ? [` content_excerpt: ${truncateExcerpt(attachment.content_excerpt, 320)}`] : []),
2646
+ ...(attachment.image_description ? [` image_description: ${truncateExcerpt(attachment.image_description, 320)}`] : []),
2647
+ ...(attachment.transcript_text ? [` transcript: ${truncateExcerpt(attachment.transcript_text, 320)}`] : []),
2648
+ ].join('\n')),
2649
+ ]
2650
+ : []),
2651
+ ].join('\n');
2652
+ }
2653
+ requireProject(alias) {
2654
+ const project = this.config.projects[alias];
2655
+ if (!project) {
2656
+ throw new Error(`Unknown project alias: ${alias}`);
2657
+ }
2658
+ return project;
2659
+ }
2660
+ async resolveProjectAlias(selectionKey) {
2661
+ const selection = await this.sessionStore.getConversation(selectionKey);
2662
+ if (selection?.selected_project_alias) {
2663
+ return selection.selected_project_alias;
2664
+ }
2665
+ const firstAlias = Object.keys(this.config.projects)[0];
2666
+ const selected = this.config.service.default_project ?? firstAlias;
2667
+ if (!selected) {
2668
+ throw new Error('No project configured.');
2669
+ }
2670
+ return selected;
2671
+ }
2672
+ async resolveProjectContext(context, selectionKey) {
2673
+ const projectAlias = await this.resolveProjectAlias(selectionKey);
2674
+ if (!canAccessProject(this.config, projectAlias, context.chat_id, 'viewer')) {
2675
+ throw new Error(`当前 chat_id 无权访问项目 ${projectAlias}。至少需要 ${describeMinimumRole('viewer')} 权限。`);
2676
+ }
2677
+ const project = this.requireProject(projectAlias);
2678
+ const sessionKey = buildConversationKey({
2679
+ tenantKey: context.tenant_key,
2680
+ chatId: context.chat_id,
2681
+ actorId: context.actor_id,
2682
+ scope: project.session_scope,
2683
+ });
2684
+ await this.sessionStore.ensureConversation(sessionKey, {
2685
+ chat_id: context.chat_id,
2686
+ actor_id: context.actor_id,
2687
+ tenant_key: context.tenant_key,
2688
+ scope: project.session_scope,
2689
+ });
2690
+ return {
2691
+ projectAlias,
2692
+ project,
2693
+ sessionKey,
2694
+ queueKey: buildQueueKey(sessionKey, projectAlias),
2695
+ };
2696
+ }
2697
+ async getSelectionConversationKey(context) {
2698
+ const scope = this.getSelectionScope(context);
2699
+ const key = buildConversationKey({
2700
+ tenantKey: context.tenant_key,
2701
+ chatId: context.chat_id,
2702
+ actorId: context.actor_id,
2703
+ scope,
2704
+ });
2705
+ await this.sessionStore.ensureConversation(key, {
2706
+ chat_id: context.chat_id,
2707
+ actor_id: context.actor_id,
2708
+ tenant_key: context.tenant_key,
2709
+ scope,
2710
+ });
2711
+ return key;
2712
+ }
2713
+ getSelectionScope(_context) {
2714
+ // Project routing is shared by chat_id so a group can keep one project binding
2715
+ // and `/project <alias>` updates the binding for the whole chat.
2716
+ return 'chat';
2717
+ }
2718
+ shouldRequireMention(project) {
2719
+ return project.mention_required || this.config.security.require_group_mentions;
2720
+ }
2721
+ resolveMemoryTarget(context, requestedScope) {
2722
+ if (requestedScope === 'group') {
2723
+ if (!this.config.service.memory_group_enabled) {
2724
+ throw new Error('群共享记忆未启用。请在配置中设置 `service.memory_group_enabled = true`。');
2725
+ }
2726
+ if (context.chat_type !== 'group') {
2727
+ throw new Error('群共享记忆只能在群聊中使用。');
2728
+ }
2729
+ return {
2730
+ scope: 'group',
2731
+ chatId: context.chat_id,
2732
+ label: '群共享记忆',
2733
+ };
2734
+ }
2735
+ return {
2736
+ scope: 'project',
2737
+ label: '项目记忆',
2738
+ };
2739
+ }
2740
+ buildMemoryExpiresAt() {
2741
+ const ttlDays = this.config.service.memory_default_ttl_days;
2742
+ if (!ttlDays) {
2743
+ return undefined;
2744
+ }
2745
+ return new Date(Date.now() + ttlDays * 24 * 60 * 60 * 1000).toISOString();
2746
+ }
2747
+ async cancelActiveRun(queueKey, reason) {
2748
+ const live = this.activeRuns.get(queueKey);
2749
+ if (live) {
2750
+ live.cancelReason = reason;
2751
+ live.controller.abort(reason === 'user' ? 'Cancelled by user' : 'Recovered stale run');
2752
+ if (live.pid) {
2753
+ terminateProcess(live.pid, 'SIGTERM');
2754
+ }
2755
+ return true;
2756
+ }
2757
+ const persisted = await this.runStateStore.getActiveRun(queueKey);
2758
+ if (!persisted?.pid || !isProcessAlive(persisted.pid)) {
2759
+ return false;
2760
+ }
2761
+ await this.runStateStore.upsertRun(persisted.run_id, {
2762
+ queue_key: persisted.queue_key,
2763
+ conversation_key: persisted.conversation_key,
2764
+ project_alias: persisted.project_alias,
2765
+ chat_id: persisted.chat_id,
2766
+ actor_id: persisted.actor_id,
2767
+ session_id: persisted.session_id,
2768
+ pid: persisted.pid,
2769
+ prompt_excerpt: persisted.prompt_excerpt,
2770
+ status: 'cancelled',
2771
+ error: 'Cancelled from runtime management command',
2772
+ });
2773
+ return terminateProcess(persisted.pid, 'SIGTERM');
2774
+ }
2775
+ async scheduleProjectExecution(projectContext, metadata, task) {
2776
+ const runId = randomUUID();
2777
+ const queued = await this.prepareQueuedExecution(projectContext, metadata, runId);
2778
+ const rootKey = buildProjectRootQueueKey(projectContext.project.root);
2779
+ const startGate = createDeferred();
2780
+ // Record queue depth when enqueuing
2781
+ this.metrics?.recordQueueDepth(projectContext.projectAlias, this.queue.getPendingCount(projectContext.queueKey) + 1);
2782
+ return {
2783
+ runId,
2784
+ queued,
2785
+ release: () => startGate.resolve(),
2786
+ completion: this.queue.run(projectContext.queueKey, async () => {
2787
+ await this.projectRootQueue.run(rootKey, async () => {
2788
+ await startGate.promise;
2789
+ await task(runId);
2790
+ }, { priority: projectContext.project.run_priority });
2791
+ // Record queue depth after dequeue
2792
+ this.metrics?.recordQueueDepth(projectContext.projectAlias, this.queue.getPendingCount(projectContext.queueKey));
2793
+ }),
2794
+ };
2795
+ }
2796
+ async prepareQueuedExecution(projectContext, metadata, runId) {
2797
+ const queuePending = this.queue.getPendingCount(projectContext.queueKey);
2798
+ const rootKey = buildProjectRootQueueKey(projectContext.project.root);
2799
+ const rootPending = this.projectRootQueue.getPendingCount(rootKey);
2800
+ if (queuePending <= 0 && rootPending <= 0) {
2801
+ return null;
2802
+ }
2803
+ const projectRoot = this.resolveProjectRoot(projectContext.project);
2804
+ const reason = queuePending > 0 ? 'project' : 'project-root';
2805
+ const frontCount = reason === 'project' ? queuePending : rootPending;
2806
+ const blockingRun = reason === 'project'
2807
+ ? await this.runStateStore.getActiveRun(projectContext.queueKey)
2808
+ : await this.runStateStore.getExecutionRunByProjectRoot(projectRoot);
2809
+ const detail = this.buildQueuedStatusDetail(projectContext.projectAlias, reason, frontCount, blockingRun);
2810
+ await this.runStateStore.upsertRun(runId, {
2811
+ queue_key: projectContext.queueKey,
2812
+ conversation_key: projectContext.sessionKey,
2813
+ project_alias: projectContext.projectAlias,
2814
+ chat_id: metadata.chatId,
2815
+ actor_id: metadata.actorId,
2816
+ project_root: projectRoot,
2817
+ prompt_excerpt: truncateExcerpt(metadata.prompt),
2818
+ status: 'queued',
2819
+ status_detail: detail,
2820
+ });
2821
+ await this.auditLog.append({
2822
+ type: 'codex.run.queued',
2823
+ run_id: runId,
2824
+ chat_id: metadata.chatId,
2825
+ actor_id: metadata.actorId,
2826
+ project_alias: projectContext.projectAlias,
2827
+ conversation_key: projectContext.sessionKey,
2828
+ project_root: projectRoot,
2829
+ queue_reason: reason,
2830
+ blocking_run_id: blockingRun?.run_id,
2831
+ front_count: frontCount,
2832
+ });
2833
+ this.logger.warn({
2834
+ runId,
2835
+ queueKey: projectContext.queueKey,
2836
+ sessionKey: projectContext.sessionKey,
2837
+ projectAlias: projectContext.projectAlias,
2838
+ projectRoot,
2839
+ reason,
2840
+ frontCount,
2841
+ blockingStatus: blockingRun?.status,
2842
+ blockingProjectAlias: blockingRun?.project_alias,
2843
+ }, 'Codex run queued');
2844
+ return {
2845
+ runId,
2846
+ detail,
2847
+ reason,
2848
+ };
2849
+ }
2850
+ buildAcknowledgedRunReply(projectAlias, phase, detail, mode) {
2851
+ if (mode === 'text') {
2852
+ return [`项目: ${projectAlias}`, `状态: ${phase}`, '', detail].join('\n');
2853
+ }
2854
+ return detail;
2855
+ }
2856
+ buildQueuedStatusDetail(projectAlias, reason, frontCount, blockingRun) {
2857
+ const lines = [
2858
+ reason === 'project' ? `当前项目 ${projectAlias} 已有任务在处理,已进入排队。` : '当前仓库正在被其他会话操作,已进入排队。',
2859
+ frontCount > 0 ? `前方还有 ${frontCount} 个任务。` : null,
2860
+ blockingRun ? `阻塞状态: ${blockingRun.status}` : null,
2861
+ reason === 'project-root' && blockingRun?.project_alias && blockingRun.project_alias !== projectAlias ? `占用项目: ${blockingRun.project_alias}` : null,
2862
+ ];
2863
+ return lines.filter(Boolean).join('\n');
2864
+ }
2865
+ buildRunStatusSummary(lastResponseExcerpt, activeRun) {
2866
+ if (activeRun?.status === 'queued' && activeRun.status_detail) {
2867
+ return [activeRun.status_detail, lastResponseExcerpt ? `\n上一轮摘要:\n${lastResponseExcerpt}` : null].filter(Boolean).join('\n');
2868
+ }
2869
+ return lastResponseExcerpt ?? '暂无会话摘要。';
2870
+ }
2871
+ isAdminChat(chatId) {
2872
+ return this.config.security.admin_chat_ids.includes(chatId);
2873
+ }
2874
+ async buildAdminStatusText() {
2875
+ const snapshots = await this.configHistoryStore.listSnapshots();
2876
+ return [
2877
+ '管理员配置',
2878
+ '',
2879
+ `viewer chat_id 数: ${this.config.security.viewer_chat_ids?.length ?? 0}`,
2880
+ `operator chat_id 数: ${this.config.security.operator_chat_ids?.length ?? 0}`,
2881
+ `管理员 chat_id 数: ${this.config.security.admin_chat_ids.length}`,
2882
+ `service observer chat_id 数: ${this.config.security.service_observer_chat_ids?.length ?? 0}`,
2883
+ `service restart chat_id 数: ${this.config.security.service_restart_chat_ids?.length ?? 0}`,
2884
+ `config admin chat_id 数: ${this.config.security.config_admin_chat_ids?.length ?? 0}`,
2885
+ `允许私聊数: ${this.config.feishu.allowed_chat_ids.length}`,
2886
+ `允许群聊数: ${this.config.feishu.allowed_group_ids.length}`,
2887
+ `项目数: ${Object.keys(this.config.projects).length}`,
2888
+ `默认项目: ${this.config.service.default_project ?? '未设置'}`,
2889
+ `回复模式: ${this.config.service.reply_mode}`,
2890
+ `配置快照数: ${snapshots.length}`,
2891
+ `可写配置: ${this.runtimeControl?.configPath ?? '无'}`,
2892
+ ].join('\n');
2893
+ }
2894
+ buildAdminListText(resource) {
2895
+ const items = resource === 'viewer'
2896
+ ? (this.config.security.viewer_chat_ids ?? [])
2897
+ : resource === 'operator'
2898
+ ? (this.config.security.operator_chat_ids ?? [])
2899
+ : resource === 'service-observer'
2900
+ ? (this.config.security.service_observer_chat_ids ?? [])
2901
+ : resource === 'service-restart'
2902
+ ? (this.config.security.service_restart_chat_ids ?? [])
2903
+ : resource === 'config-admin'
2904
+ ? (this.config.security.config_admin_chat_ids ?? [])
2905
+ : resource === 'admin'
2906
+ ? this.config.security.admin_chat_ids
2907
+ : resource === 'group'
2908
+ ? this.config.feishu.allowed_group_ids
2909
+ : this.config.feishu.allowed_chat_ids;
2910
+ return [`当前${resource}列表:`, ...(items.length > 0 ? items : ['(empty)'])].join('\n');
2911
+ }
2912
+ buildProjectsAdminText(allowedAliases) {
2913
+ const entries = Object.entries(this.config.projects).filter(([alias]) => !allowedAliases || allowedAliases.has(alias));
2914
+ const lines = entries.map(([alias, project]) => {
2915
+ const flags = [`scope=${project.session_scope}`, `mention=${project.mention_required ? 'on' : 'off'}`].join(' ');
2916
+ const roles = [
2917
+ `viewer=${project.viewer_chat_ids?.length ?? 0}`,
2918
+ `operator=${project.operator_chat_ids?.length ?? 0}`,
2919
+ `admin=${project.admin_chat_ids.length}`,
2920
+ `session_operator=${project.session_operator_chat_ids?.length ?? 0}`,
2921
+ `run_operator=${project.run_operator_chat_ids?.length ?? 0}`,
2922
+ `config_admin=${project.config_admin_chat_ids?.length ?? 0}`,
2923
+ ].join(' ');
2924
+ return `- ${alias}: ${project.root} | ${flags} | ${roles}`;
2925
+ });
2926
+ return ['当前项目列表:', ...(lines.length > 0 ? lines : ['(empty)'])].join('\n');
2927
+ }
2928
+ async buildAdminRunsText(allowedAliases) {
2929
+ const runs = await this.runStateStore.listRuns();
2930
+ const visibleRuns = allowedAliases ? runs.filter((run) => allowedAliases.has(run.project_alias)) : runs;
2931
+ const active = visibleRuns.filter((run) => isVisibleRunStatus(run.status)).slice(0, 10);
2932
+ const recentFailures = visibleRuns.filter((run) => run.status === 'failure' || run.status === 'cancelled' || run.status === 'stale').slice(0, 5);
2933
+ const lines = ['当前运行列表'];
2934
+ if (active.length === 0) {
2935
+ lines.push('', 'active/queued: (empty)');
2936
+ }
2937
+ else {
2938
+ lines.push('', 'active/queued:');
2939
+ for (const run of active) {
2940
+ lines.push(`- ${run.project_alias} | ${run.status} | chat=${run.chat_id} | 已持续 ${formatAgeFromNow(run.started_at)}${run.project_root ? ` | root=${run.project_root}` : ''}`);
2941
+ lines.push(` prompt=${truncateExcerpt(run.prompt_excerpt, 80)}`);
2942
+ if (run.status_detail) {
2943
+ lines.push(` detail=${truncateExcerpt(run.status_detail, 120)}`);
2944
+ }
2945
+ }
2946
+ }
2947
+ if (recentFailures.length > 0) {
2948
+ lines.push('', '最近失败:');
2949
+ for (const run of recentFailures) {
2950
+ lines.push(`- ${run.project_alias} | ${run.status} | ${run.updated_at}`);
2951
+ lines.push(` error=${truncateExcerpt(run.error ?? 'unknown', 120)}`);
2952
+ }
2953
+ }
2954
+ return lines.join('\n');
2955
+ }
2956
+ getAuthorizedProjectAliases(chatId, minimumRole = 'admin') {
2957
+ return Object.keys(this.config.projects).filter((alias) => canAccessProject(this.config, alias, chatId, minimumRole));
2958
+ }
2959
+ isProjectAdminChat(chatId, projectAlias) {
2960
+ return canAccessProjectCapability(this.config, projectAlias, chatId, 'project:mutate');
2961
+ }
2962
+ canControlProjectSessions(chatId, projectAlias) {
2963
+ return canAccessProjectCapability(this.config, projectAlias, chatId, 'session:control');
2964
+ }
2965
+ canExecuteProjectRuns(chatId, projectAlias) {
2966
+ return canAccessProjectCapability(this.config, projectAlias, chatId, 'run:execute');
2967
+ }
2968
+ canCancelProjectRuns(chatId, projectAlias) {
2969
+ return canAccessProjectCapability(this.config, projectAlias, chatId, 'run:cancel');
2970
+ }
2971
+ canObserveService(chatId) {
2972
+ return canAccessGlobalCapability(this.config, chatId, 'service:status');
2973
+ }
2974
+ canObserveRuns(chatId) {
2975
+ return canAccessGlobalCapability(this.config, chatId, 'service:runs');
2976
+ }
2977
+ canRestartService(chatId) {
2978
+ return canAccessGlobalCapability(this.config, chatId, 'service:restart');
2979
+ }
2980
+ canMutateRuntimeConfig(chatId) {
2981
+ return canAccessGlobalCapability(this.config, chatId, 'config:mutate');
2982
+ }
2983
+ canReadConfigHistory(chatId) {
2984
+ return canAccessGlobalCapability(this.config, chatId, 'config:history') || this.canMutateRuntimeConfig(chatId);
2985
+ }
2986
+ canAccessAdminCommand(command, currentProjectAlias, authorizedProjectAliases, operatorProjectAliases, globalCapabilities) {
2987
+ if (command.resource === 'project') {
2988
+ if (command.action === 'list') {
2989
+ return globalCapabilities.serviceObserver || operatorProjectAliases.length > 0;
2990
+ }
2991
+ if (command.action === 'set' && command.alias) {
2992
+ return globalCapabilities.globalConfigAdmin || authorizedProjectAliases.includes(command.alias);
2993
+ }
2994
+ return globalCapabilities.globalConfigAdmin;
2995
+ }
2996
+ if (command.resource === 'service') {
2997
+ if (command.action === 'restart') {
2998
+ return globalCapabilities.serviceRestarter;
2999
+ }
3000
+ return globalCapabilities.serviceObserver || operatorProjectAliases.length > 0;
3001
+ }
3002
+ if (command.resource === 'config') {
3003
+ return globalCapabilities.globalConfigAdmin;
3004
+ }
3005
+ return authorizedProjectAliases.includes(currentProjectAlias);
3006
+ }
3007
+ commandRequiresWritableConfig(command) {
3008
+ if (command.resource === 'service') {
3009
+ return command.action === 'restart';
3010
+ }
3011
+ return true;
3012
+ }
3013
+ async handleAdminConfigCommand(context, command) {
3014
+ if (command.action === 'history' && !this.canReadConfigHistory(context.chat_id)) {
3015
+ await this.sendTextReply(context.chat_id, '当前 chat_id 无权查看配置历史。', context.message_id, context.text);
3016
+ return;
3017
+ }
3018
+ if (command.action === 'rollback' && !this.canMutateRuntimeConfig(context.chat_id)) {
3019
+ await this.sendTextReply(context.chat_id, '当前 chat_id 无权回滚配置。', context.message_id, context.text);
3020
+ return;
3021
+ }
3022
+ if (!this.runtimeControl?.configPath) {
3023
+ await this.sendTextReply(context.chat_id, '当前运行实例没有可写配置路径,无法执行配置历史操作。', context.message_id, context.text);
3024
+ return;
3025
+ }
3026
+ if (command.action === 'history') {
3027
+ const snapshots = await this.configHistoryStore.listSnapshots();
3028
+ if (snapshots.length === 0) {
3029
+ await this.sendTextReply(context.chat_id, '当前没有可回滚的配置快照。', context.message_id, context.text);
3030
+ return;
3031
+ }
3032
+ const lines = ['最近配置快照:'];
3033
+ for (const snapshot of snapshots) {
3034
+ lines.push(`- ${snapshot.id} | ${snapshot.at} | ${snapshot.action}${snapshot.summary ? ` | ${snapshot.summary}` : ''}`);
3035
+ }
3036
+ await this.sendTextReply(context.chat_id, lines.join('\n'), context.message_id, context.text);
3037
+ return;
3038
+ }
3039
+ const target = await this.configHistoryStore.getSnapshot(command.value);
3040
+ if (!target) {
3041
+ await this.sendTextReply(context.chat_id, '未找到指定配置快照。可先执行 `/admin config history`。', context.message_id, context.text);
3042
+ return;
3043
+ }
3044
+ const rollbackSnapshot = await this.snapshotConfigForAdminMutation(context, 'config.rollback', `rollback -> ${target.id}`);
3045
+ const previousContent = rollbackSnapshot.content;
3046
+ try {
3047
+ await writeUtf8Atomic(this.runtimeControl.configPath, target.content);
3048
+ await this.reloadRuntimeConfigFromDisk(this.runtimeControl.configPath);
3049
+ }
3050
+ catch (error) {
3051
+ await writeUtf8Atomic(this.runtimeControl.configPath, previousContent);
3052
+ await this.reloadRuntimeConfigFromDisk(this.runtimeControl.configPath);
3053
+ throw error;
3054
+ }
3055
+ await this.appendAdminAudit({
3056
+ type: 'admin.config.rollback',
3057
+ chat_id: context.chat_id,
3058
+ actor_id: context.actor_id,
3059
+ target_snapshot_id: target.id,
3060
+ snapshot_id: rollbackSnapshot.id,
3061
+ config_path: this.runtimeControl.configPath,
3062
+ });
3063
+ await this.sendTextReply(context.chat_id, `已回滚配置。\n目标快照: ${target.id}\n回滚前快照: ${rollbackSnapshot.id}\n如需生效到某些运行时状态,请再执行 /admin service restart。`, context.message_id, context.text);
3064
+ }
3065
+ async snapshotConfigForAdminMutation(context, action, summary) {
3066
+ if (!this.runtimeControl?.configPath) {
3067
+ throw new Error('Runtime config path is unavailable');
3068
+ }
3069
+ return this.configHistoryStore.recordSnapshot({
3070
+ configPath: this.runtimeControl.configPath,
3071
+ action,
3072
+ summary,
3073
+ chatId: context.chat_id,
3074
+ actorId: context.actor_id,
3075
+ limit: 5,
3076
+ });
3077
+ }
3078
+ async appendAdminAudit(event) {
3079
+ await this.adminAuditLog.append(event);
3080
+ }
3081
+ async reloadRuntimeConfigFromDisk(configPath) {
3082
+ const { config: nextConfig } = await loadBridgeConfigFile(configPath);
3083
+ replaceObject(this.config.service, nextConfig.service);
3084
+ replaceObject(this.config.codex, nextConfig.codex);
3085
+ replaceObject(this.config.storage, nextConfig.storage);
3086
+ replaceObject(this.config.security, nextConfig.security);
3087
+ replaceObject(this.config.feishu, nextConfig.feishu);
3088
+ replaceProjects(this.config.projects, nextConfig.projects);
3089
+ }
3090
+ parseProjectPatch(field, value) {
3091
+ switch (field) {
3092
+ case 'root':
3093
+ return { root: value };
3094
+ case 'profile':
3095
+ return { profile: value };
3096
+ case 'sandbox':
3097
+ if (value === 'read-only' || value === 'workspace-write' || value === 'danger-full-access') {
3098
+ return { sandbox: value };
3099
+ }
3100
+ return null;
3101
+ case 'session_scope':
3102
+ if (value === 'chat' || value === 'chat-user') {
3103
+ return { session_scope: value };
3104
+ }
3105
+ return null;
3106
+ case 'mention_required':
3107
+ if (value === 'true' || value === 'false') {
3108
+ return { mention_required: value === 'true' };
3109
+ }
3110
+ return null;
3111
+ case 'description':
3112
+ return { description: value };
3113
+ case 'viewer_chat_ids':
3114
+ return { viewer_chat_ids: splitCommaSeparatedValues(value) };
3115
+ case 'operator_chat_ids':
3116
+ return { operator_chat_ids: splitCommaSeparatedValues(value) };
3117
+ case 'admin_chat_ids':
3118
+ return { admin_chat_ids: splitCommaSeparatedValues(value) };
3119
+ case 'session_operator_chat_ids':
3120
+ return { session_operator_chat_ids: splitCommaSeparatedValues(value) };
3121
+ case 'run_operator_chat_ids':
3122
+ return { run_operator_chat_ids: splitCommaSeparatedValues(value) };
3123
+ case 'config_admin_chat_ids':
3124
+ return { config_admin_chat_ids: splitCommaSeparatedValues(value) };
3125
+ case 'download_dir':
3126
+ return { download_dir: value };
3127
+ case 'temp_dir':
3128
+ return { temp_dir: value };
3129
+ case 'cache_dir':
3130
+ return { cache_dir: value };
3131
+ case 'log_dir':
3132
+ return { log_dir: value };
3133
+ case 'run_priority': {
3134
+ const parsed = Number(value);
3135
+ return Number.isInteger(parsed) && parsed >= 1 && parsed <= 1000 ? { run_priority: parsed } : null;
3136
+ }
3137
+ case 'chat_rate_limit_window_seconds': {
3138
+ const parsed = Number(value);
3139
+ return Number.isInteger(parsed) && parsed > 0 ? { chat_rate_limit_window_seconds: parsed } : null;
3140
+ }
3141
+ case 'chat_rate_limit_max_runs': {
3142
+ const parsed = Number(value);
3143
+ return Number.isInteger(parsed) && parsed > 0 ? { chat_rate_limit_max_runs: parsed } : null;
3144
+ }
3145
+ default:
3146
+ return null;
3147
+ }
3148
+ }
3149
+ resolveProjectDownloadDir(projectAlias, project) {
3150
+ return getProjectDownloadsDir(this.config.storage.dir, projectAlias, project);
3151
+ }
3152
+ resolveProjectTempDir(projectAlias, project) {
3153
+ return getProjectTempDir(this.config.storage.dir, projectAlias, project);
3154
+ }
3155
+ resolveProjectCacheDir(projectAlias, project) {
3156
+ return getProjectCacheDir(this.config.storage.dir, projectAlias, project);
3157
+ }
3158
+ async appendProjectAuditEvent(projectAlias, project, event) {
3159
+ const auditLog = new AuditLog(getProjectAuditDir(this.config.storage.dir, projectAlias, project), 'project-audit.jsonl');
3160
+ await auditLog.append(event);
3161
+ }
3162
+ async notifyProjectChats(projectAlias, text) {
3163
+ const project = this.config.projects[projectAlias];
3164
+ const chatIds = project?.notification_chat_ids ?? [];
3165
+ for (const chatId of chatIds) {
3166
+ try {
3167
+ await this.feishuClient.sendText(chatId, text);
3168
+ }
3169
+ catch { /* best-effort */ }
3170
+ }
3171
+ }
3172
+ listManagedAuditTargets() {
3173
+ const targets = [
3174
+ {
3175
+ stateDir: this.config.storage.dir,
3176
+ fileName: 'audit.jsonl',
3177
+ archiveDir: path.join(this.config.storage.dir, 'archive'),
3178
+ },
3179
+ {
3180
+ stateDir: this.config.storage.dir,
3181
+ fileName: 'admin-audit.jsonl',
3182
+ archiveDir: path.join(this.config.storage.dir, 'archive'),
3183
+ },
3184
+ ];
3185
+ for (const [alias, project] of Object.entries(this.config.projects)) {
3186
+ targets.push({
3187
+ stateDir: getProjectAuditDir(this.config.storage.dir, alias, project),
3188
+ fileName: path.basename(getProjectAuditFile(this.config.storage.dir, alias, project)),
3189
+ archiveDir: getProjectArchiveDir(this.config.storage.dir, alias),
3190
+ });
3191
+ }
3192
+ return targets;
3193
+ }
3194
+ checkAndConsumeChatRateLimit(projectAlias, project, chatId) {
3195
+ const windowMs = project.chat_rate_limit_window_seconds * 1000;
3196
+ const maxRuns = project.chat_rate_limit_max_runs;
3197
+ const key = `${projectAlias}::${chatId}`;
3198
+ const now = Date.now();
3199
+ const recent = (this.chatRateWindows.get(key) ?? []).filter((timestamp) => now - timestamp < windowMs);
3200
+ if (recent.length >= maxRuns) {
3201
+ const retryAfterSeconds = Math.max(1, Math.ceil((windowMs - (now - recent[0])) / 1000));
3202
+ this.chatRateWindows.set(key, recent);
3203
+ return [
3204
+ '消息接收: rejected',
3205
+ '处理状态: rate_limited',
3206
+ `项目: ${projectAlias}`,
3207
+ '',
3208
+ `当前 chat 在 ${project.chat_rate_limit_window_seconds} 秒内最多提交 ${project.chat_rate_limit_max_runs} 次运行。`,
3209
+ `请约 ${retryAfterSeconds} 秒后再试。`,
3210
+ ].join('\n');
3211
+ }
3212
+ recent.push(now);
3213
+ this.chatRateWindows.set(key, recent);
3214
+ return null;
3215
+ }
3216
+ applyAdminListValues(resource, values) {
3217
+ if (resource === 'viewer') {
3218
+ this.config.security.viewer_chat_ids = values;
3219
+ return;
3220
+ }
3221
+ if (resource === 'operator') {
3222
+ this.config.security.operator_chat_ids = values;
3223
+ return;
3224
+ }
3225
+ if (resource === 'service-observer') {
3226
+ this.config.security.service_observer_chat_ids = values;
3227
+ return;
3228
+ }
3229
+ if (resource === 'service-restart') {
3230
+ this.config.security.service_restart_chat_ids = values;
3231
+ return;
3232
+ }
3233
+ if (resource === 'config-admin') {
3234
+ this.config.security.config_admin_chat_ids = values;
3235
+ return;
3236
+ }
3237
+ if (resource === 'admin') {
3238
+ this.config.security.admin_chat_ids = values;
3239
+ return;
3240
+ }
3241
+ if (resource === 'group') {
3242
+ this.config.feishu.allowed_group_ids = values;
3243
+ return;
3244
+ }
3245
+ this.config.feishu.allowed_chat_ids = values;
3246
+ }
3247
+ resolveProjectRoot(project) {
3248
+ return path.resolve(project.root);
3249
+ }
3250
+ resolveBackendByName(projectAlias, sessionOverride) {
3251
+ return resolveProjectBackendWithOverride(this.config, projectAlias, sessionOverride, this.codexSessionIndex);
3252
+ }
3253
+ async enforceSessionHistoryLimit(conversationKey, projectAlias) {
3254
+ const sessions = await this.sessionStore.listProjectSessions(conversationKey, projectAlias);
3255
+ const overflow = sessions.slice(this.config.service.session_history_limit);
3256
+ for (const session of overflow) {
3257
+ await this.sessionStore.dropProjectSession(conversationKey, projectAlias, session.thread_id);
3258
+ }
3259
+ }
3260
+ async sendTextReply(chatId, body, replyToMessageId, originalText, presentation) {
3261
+ const title = this.buildReplyTitle(this.sanitizeUserVisibleReply(body));
3262
+ const formattedBody = this.sanitizeUserVisibleReply(this.formatQuotedReply(body, originalText));
3263
+ if (this.config.service.reply_mode === 'card') {
3264
+ const card = buildMessageCard({
3265
+ title,
3266
+ body: formattedBody,
3267
+ status: presentation?.status,
3268
+ phase: presentation?.phase,
3269
+ projectAlias: presentation?.projectAlias,
3270
+ });
3271
+ const response = await this.sendCardReply(chatId, card, replyToMessageId);
3272
+ await this.auditLog.append({
3273
+ type: 'message.replied',
3274
+ chat_id: chatId,
3275
+ reply_mode: 'card',
3276
+ reply_to_message_id: replyToMessageId,
3277
+ title,
3278
+ });
3279
+ return response;
3280
+ }
3281
+ if (this.config.service.reply_mode === 'post') {
3282
+ const post = buildFeishuPost(title, formattedBody);
3283
+ if (this.config.service.reply_quote_user_message && replyToMessageId) {
3284
+ const response = await this.feishuClient.sendPost(chatId, post, { replyToMessageId });
3285
+ await this.auditLog.append({
3286
+ type: 'message.replied',
3287
+ chat_id: chatId,
3288
+ reply_mode: 'post',
3289
+ reply_to_message_id: replyToMessageId,
3290
+ title,
3291
+ });
3292
+ return response;
3293
+ }
3294
+ const response = await this.feishuClient.sendPost(chatId, post);
3295
+ await this.auditLog.append({
3296
+ type: 'message.replied',
3297
+ chat_id: chatId,
3298
+ reply_mode: 'post',
3299
+ title,
3300
+ });
3301
+ return response;
3302
+ }
3303
+ if (this.config.service.reply_quote_user_message && replyToMessageId) {
3304
+ const response = await this.feishuClient.sendText(chatId, this.sanitizeUserVisibleReply(body), { replyToMessageId });
3305
+ await this.auditLog.append({
3306
+ type: 'message.replied',
3307
+ chat_id: chatId,
3308
+ reply_mode: 'text',
3309
+ reply_to_message_id: replyToMessageId,
3310
+ title,
3311
+ });
3312
+ return response;
3313
+ }
3314
+ const response = await this.feishuClient.sendText(chatId, formattedBody);
3315
+ await this.auditLog.append({
3316
+ type: 'message.replied',
3317
+ chat_id: chatId,
3318
+ reply_mode: 'text',
3319
+ title,
3320
+ });
3321
+ return response;
3322
+ }
3323
+ async sendCardReply(chatId, card, replyToMessageId) {
3324
+ if (this.config.service.reply_quote_user_message && replyToMessageId) {
3325
+ return this.feishuClient.sendCard(chatId, card, { replyToMessageId });
3326
+ }
3327
+ return this.feishuClient.sendCard(chatId, card);
3328
+ }
3329
+ async sendRunLifecycleReply(input) {
3330
+ const lifecycleMode = this.resolveRunLifecycleReplyMode();
3331
+ const lifecycleReplyOptions = input.replyToMessageId ? { replyToMessageId: input.replyToMessageId } : undefined;
3332
+ if (lifecycleMode === 'card') {
3333
+ const card = this.buildRunLifecycleCard({
3334
+ title: input.title,
3335
+ body: input.body,
3336
+ projectAlias: input.projectAlias,
3337
+ runStatus: input.runStatus,
3338
+ runPhase: input.runPhase,
3339
+ });
3340
+ const response = lifecycleReplyOptions
3341
+ ? await this.feishuClient.sendCard(input.chatId, card, lifecycleReplyOptions)
3342
+ : await this.sendCardReply(input.chatId, card);
3343
+ await this.auditLog.append({
3344
+ type: 'codex.run.replied',
3345
+ chat_id: input.chatId,
3346
+ project_alias: input.projectAlias,
3347
+ run_status: input.runStatus,
3348
+ run_phase: input.runPhase,
3349
+ ...(input.runId ? { run_id: input.runId } : {}),
3350
+ });
3351
+ return response;
3352
+ }
3353
+ if (lifecycleMode === 'post') {
3354
+ const postBody = this.sanitizeUserVisibleReply(this.formatQuotedReply(input.body, input.originalText));
3355
+ const title = this.buildReplyTitle(postBody);
3356
+ const post = buildFeishuPost(title, postBody);
3357
+ const response = lifecycleReplyOptions
3358
+ ? await this.feishuClient.sendPost(input.chatId, post, lifecycleReplyOptions)
3359
+ : await this.feishuClient.sendPost(input.chatId, post);
3360
+ await this.auditLog.append({
3361
+ type: 'codex.run.replied',
3362
+ chat_id: input.chatId,
3363
+ project_alias: input.projectAlias,
3364
+ run_status: input.runStatus,
3365
+ run_phase: input.runPhase,
3366
+ ...(input.runId ? { run_id: input.runId } : {}),
3367
+ });
3368
+ return response;
3369
+ }
3370
+ const response = lifecycleReplyOptions
3371
+ ? await this.feishuClient.sendText(input.chatId, this.sanitizeUserVisibleReply(this.formatQuotedReply(input.body, input.originalText)), lifecycleReplyOptions)
3372
+ : await this.sendTextReply(input.chatId, input.body, input.replyToMessageId, input.originalText);
3373
+ await this.auditLog.append({
3374
+ type: 'codex.run.replied',
3375
+ chat_id: input.chatId,
3376
+ project_alias: input.projectAlias,
3377
+ run_status: input.runStatus,
3378
+ run_phase: input.runPhase,
3379
+ ...(input.runId ? { run_id: input.runId } : {}),
3380
+ });
3381
+ return response;
3382
+ }
3383
+ buildInitialRunLifecycleReply(projectAlias, queued, mode) {
3384
+ if (queued) {
3385
+ return {
3386
+ title: '已加入排队',
3387
+ body: this.buildAcknowledgedRunReply(projectAlias, '排队中', queued.detail, mode),
3388
+ runStatus: 'queued',
3389
+ runPhase: '排队中',
3390
+ };
3391
+ }
3392
+ return {
3393
+ title: '已接收请求',
3394
+ body: this.buildAcknowledgedRunReply(projectAlias, '已接收', '已收到你的消息,正在准备处理。', mode),
3395
+ runStatus: 'running',
3396
+ runPhase: '已接收',
3397
+ };
3398
+ }
3399
+ async sendInitialRunLifecycleReply(input) {
3400
+ const lifecycleMode = this.resolveRunLifecycleReplyMode();
3401
+ const draft = this.buildInitialRunLifecycleReply(input.projectAlias, input.queued, lifecycleMode);
3402
+ try {
3403
+ const response = await this.sendRunLifecycleReply({
3404
+ chatId: input.chatId,
3405
+ projectAlias: input.projectAlias,
3406
+ title: draft.title,
3407
+ body: draft.body,
3408
+ runStatus: draft.runStatus,
3409
+ runPhase: draft.runPhase,
3410
+ runId: input.runId,
3411
+ replyToMessageId: input.replyToMessageId,
3412
+ originalText: input.originalText,
3413
+ });
3414
+ await this.rememberRunReplyTarget(input.runId, response, lifecycleMode);
3415
+ }
3416
+ catch (error) {
3417
+ this.logger.warn({ error, runId: input.runId, projectAlias: input.projectAlias }, 'Failed to send initial lifecycle reply');
3418
+ }
3419
+ }
3420
+ async rememberRunReplyTarget(runId, response, mode = this.resolveRunLifecycleReplyMode()) {
3421
+ this.runReplyTargets.set(runId, {
3422
+ messageId: response.message_id,
3423
+ mode,
3424
+ });
3425
+ }
3426
+ async updateRunStartedReply(chatId, projectAlias, runId) {
3427
+ const target = this.runReplyTargets.get(runId);
3428
+ if (!target?.messageId) {
3429
+ return;
3430
+ }
3431
+ const body = this.buildAcknowledgedRunReply(projectAlias, '处理中', '桥接器已开始处理你的请求。', target.mode);
3432
+ await this.updateRunLifecycleReply({
3433
+ chatId,
3434
+ projectAlias,
3435
+ title: 'Codex 处理中',
3436
+ body,
3437
+ runStatus: 'running',
3438
+ runPhase: '处理中',
3439
+ runId,
3440
+ });
3441
+ }
3442
+ async updateRunProgressReply(input, runId, progress) {
3443
+ const target = this.runReplyTargets.get(runId);
3444
+ if (!target?.messageId) {
3445
+ return;
3446
+ }
3447
+ const body = [
3448
+ this.buildAcknowledgedRunReply(input.projectAlias, '处理中', '桥接器正在持续处理你的请求。', target.mode),
3449
+ '最新进展:',
3450
+ progress,
3451
+ ]
3452
+ .filter(Boolean)
3453
+ .join('\n\n');
3454
+ const updated = await this.updateRunLifecycleReply({
3455
+ chatId: input.chatId,
3456
+ projectAlias: input.projectAlias,
3457
+ title: 'Codex 处理中',
3458
+ body,
3459
+ runStatus: 'running',
3460
+ runPhase: '生成中',
3461
+ runId,
3462
+ });
3463
+ if (!updated) {
3464
+ return;
3465
+ }
3466
+ }
3467
+ async sendOrUpdateRunOutcome(input) {
3468
+ const updated = await this.updateRunLifecycleReply({
3469
+ chatId: input.input.chatId,
3470
+ projectAlias: input.input.projectAlias,
3471
+ title: input.title,
3472
+ body: input.body,
3473
+ runStatus: input.runStatus,
3474
+ runPhase: input.runPhase,
3475
+ runId: input.runId,
3476
+ sessionKey: input.input.sessionKey,
3477
+ sessionId: input.sessionId,
3478
+ cardSummary: input.cardSummary,
3479
+ });
3480
+ if (updated) {
3481
+ return;
3482
+ }
3483
+ await this.sendTextReply(input.input.chatId, input.body, input.input.replyToMessageId, input.input.prompt, {
3484
+ status: input.runStatus,
3485
+ });
3486
+ await this.auditLog.append({
3487
+ type: 'codex.run.replied',
3488
+ chat_id: input.input.chatId,
3489
+ project_alias: input.input.projectAlias,
3490
+ run_status: input.runStatus,
3491
+ run_phase: input.runPhase,
3492
+ run_id: input.runId,
3493
+ });
3494
+ }
3495
+ async updateRunLifecycleReply(input) {
3496
+ const target = this.runReplyTargets.get(input.runId);
3497
+ if (!target?.messageId) {
3498
+ return false;
3499
+ }
3500
+ const sanitizedBody = this.sanitizeUserVisibleReply(input.body);
3501
+ if (target.mode === 'card') {
3502
+ const includeActions = input.runStatus === 'success' && this.supportsInteractiveCardActions() && input.sessionKey !== undefined;
3503
+ await this.feishuClient.updateCard(target.messageId, this.buildRunLifecycleCard({
3504
+ title: input.title,
3505
+ body: input.body,
3506
+ projectAlias: input.projectAlias,
3507
+ runStatus: input.runStatus,
3508
+ runPhase: input.runPhase,
3509
+ cardSummary: input.cardSummary,
3510
+ includeActions,
3511
+ rerunPayload: includeActions && input.sessionKey
3512
+ ? {
3513
+ action: 'rerun',
3514
+ conversation_key: input.sessionKey,
3515
+ project_alias: input.projectAlias,
3516
+ chat_id: input.chatId,
3517
+ }
3518
+ : undefined,
3519
+ newSessionPayload: includeActions && input.sessionKey
3520
+ ? {
3521
+ action: 'new',
3522
+ conversation_key: input.sessionKey,
3523
+ project_alias: input.projectAlias,
3524
+ chat_id: input.chatId,
3525
+ }
3526
+ : undefined,
3527
+ statusPayload: includeActions && input.sessionKey
3528
+ ? {
3529
+ action: 'status',
3530
+ conversation_key: input.sessionKey,
3531
+ project_alias: input.projectAlias,
3532
+ chat_id: input.chatId,
3533
+ }
3534
+ : undefined,
3535
+ }));
3536
+ }
3537
+ else if (target.mode === 'post') {
3538
+ const title = this.buildReplyTitle(sanitizedBody);
3539
+ await this.feishuClient.updatePost(target.messageId, buildFeishuPost(title, sanitizedBody));
3540
+ }
3541
+ else {
3542
+ await this.feishuClient.updateText(target.messageId, sanitizedBody);
3543
+ }
3544
+ await this.auditLog.append({
3545
+ type: 'message.updated',
3546
+ chat_id: input.chatId,
3547
+ project_alias: input.projectAlias,
3548
+ run_id: input.runId,
3549
+ run_status: input.runStatus,
3550
+ run_phase: input.runPhase,
3551
+ reply_mode: target.mode,
3552
+ });
3553
+ await this.auditLog.append({
3554
+ type: 'codex.run.replied',
3555
+ chat_id: input.chatId,
3556
+ project_alias: input.projectAlias,
3557
+ run_status: input.runStatus,
3558
+ run_phase: input.runPhase,
3559
+ run_id: input.runId,
3560
+ update: true,
3561
+ });
3562
+ return true;
3563
+ }
3564
+ formatQuotedReply(body, originalText) {
3565
+ return body;
3566
+ }
3567
+ buildReplyTitle(body) {
3568
+ const firstLine = body
3569
+ .split(/\r?\n/)
3570
+ .map((line) => line.trim())
3571
+ .find(Boolean);
3572
+ return truncateExcerpt(firstLine ?? '飞鹊 (Feique)', 40);
3573
+ }
3574
+ sanitizeUserVisibleReply(body) {
3575
+ return body
3576
+ .split(/\r?\n/)
3577
+ .filter((line) => !/^(运行|当前运行|阻塞运行|run[_ -]?id|session[_ -]?id|conversation[_ -]?key|chat[_ -]?id|tenant[_ -]?key|project[_ -]?root|pid):/i.test(line.trim()))
3578
+ .join('\n')
3579
+ .replace(/\n{3,}/g, '\n\n')
3580
+ .trim();
3581
+ }
3582
+ supportsInteractiveCardActions() {
3583
+ return this.config.feishu.transport === 'webhook';
3584
+ }
3585
+ resolveRunLifecycleReplyMode() {
3586
+ if (this.config.service.reply_mode === 'post') {
3587
+ return 'card';
3588
+ }
3589
+ return this.config.service.reply_mode;
3590
+ }
3591
+ buildRunLifecycleCard(input) {
3592
+ const sanitizedBody = this.sanitizeUserVisibleReply(input.body);
3593
+ if (input.includeActions) {
3594
+ return buildStatusCard({
3595
+ title: input.title,
3596
+ summary: input.cardSummary ?? truncateForFeishuCard(this.stripLifecycleMetadata(sanitizedBody)),
3597
+ projectAlias: input.projectAlias,
3598
+ runStatus: input.runStatus,
3599
+ runPhase: input.runPhase,
3600
+ includeActions: true,
3601
+ rerunPayload: input.rerunPayload,
3602
+ newSessionPayload: input.newSessionPayload,
3603
+ statusPayload: input.statusPayload,
3604
+ cancelPayload: input.cancelPayload,
3605
+ });
3606
+ }
3607
+ return buildMessageCard({
3608
+ title: input.title,
3609
+ body: this.stripLifecycleMetadata(sanitizedBody),
3610
+ status: input.runStatus,
3611
+ phase: input.runPhase,
3612
+ projectAlias: input.projectAlias,
3613
+ });
3614
+ }
3615
+ stripLifecycleMetadata(body) {
3616
+ return body
3617
+ .split(/\r?\n/)
3618
+ .filter((line) => !/^(项目|处理状态|会话|当前会话|已保存会话数):/.test(line.trim()))
3619
+ .join('\n')
3620
+ .replace(/\n{3,}/g, '\n\n')
3621
+ .trim();
3622
+ }
3623
+ }
3624
+ export function buildQueueKey(conversationKey, projectAlias) {
3625
+ return `${conversationKey}::project::${projectAlias}`;
3626
+ }
3627
+ export function buildProjectRootQueueKey(projectRoot) {
3628
+ return `root::${path.resolve(projectRoot)}`;
3629
+ }
3630
+ function isExecutionRunStatus(status) {
3631
+ return status === 'running' || status === 'orphaned';
3632
+ }
3633
+ function isVisibleRunStatus(status) {
3634
+ return status === 'queued' || isExecutionRunStatus(status);
3635
+ }
3636
+ function buildMessageDedupeKey(context) {
3637
+ return ['message', context.tenant_key ?? 'tenant', context.chat_id, context.message_id].join('::');
3638
+ }
3639
+ function buildCardDedupeKey(context, action) {
3640
+ if (!context.open_message_id) {
3641
+ return null;
3642
+ }
3643
+ return ['card', context.tenant_key ?? 'tenant', context.chat_id ?? 'chat', context.actor_id ?? 'actor', context.open_message_id, action].join('::');
3644
+ }
3645
+ function truncateExcerpt(text, limit = 160) {
3646
+ return text.length > limit ? `${text.slice(0, limit)}...` : text;
3647
+ }
3648
+ function splitCommaSeparatedValues(value) {
3649
+ return value
3650
+ .split(',')
3651
+ .map((item) => item.trim())
3652
+ .filter(Boolean);
3653
+ }
3654
+ function resolveAdminListTarget(resource) {
3655
+ switch (resource) {
3656
+ case 'viewer':
3657
+ return { section: 'security', key: 'viewer_chat_ids' };
3658
+ case 'operator':
3659
+ return { section: 'security', key: 'operator_chat_ids' };
3660
+ case 'admin':
3661
+ return { section: 'security', key: 'admin_chat_ids' };
3662
+ case 'service-observer':
3663
+ return { section: 'security', key: 'service_observer_chat_ids' };
3664
+ case 'service-restart':
3665
+ return { section: 'security', key: 'service_restart_chat_ids' };
3666
+ case 'config-admin':
3667
+ return { section: 'security', key: 'config_admin_chat_ids' };
3668
+ case 'group':
3669
+ return { section: 'feishu', key: 'allowed_group_ids' };
3670
+ case 'chat':
3671
+ return { section: 'feishu', key: 'allowed_chat_ids' };
3672
+ }
3673
+ }
3674
+ function buildConversationKeyForConversation(conversation) {
3675
+ return buildConversationKey({
3676
+ tenantKey: conversation.tenant_key,
3677
+ chatId: conversation.chat_id,
3678
+ actorId: conversation.actor_id,
3679
+ scope: conversation.scope,
3680
+ });
3681
+ }
3682
+ function renderMemorySection(title, items, budget) {
3683
+ if (items.length === 0) {
3684
+ return [];
3685
+ }
3686
+ const lines = ['', title];
3687
+ let used = 0;
3688
+ for (const item of items) {
3689
+ const line = `- ${item.title}${item.pinned ? ' [pinned]' : ''}: ${item.content}`;
3690
+ if (used + line.length > budget) {
3691
+ break;
3692
+ }
3693
+ lines.push(truncateExcerpt(line, 280));
3694
+ used += line.length;
3695
+ }
3696
+ return lines.length > 2 ? lines : [];
3697
+ }
3698
+ function formatAgeFromNow(isoTimestamp) {
3699
+ const deltaMs = Date.now() - Date.parse(isoTimestamp);
3700
+ if (!Number.isFinite(deltaMs) || deltaMs < 0) {
3701
+ return '0s';
3702
+ }
3703
+ const totalSeconds = Math.floor(deltaMs / 1000);
3704
+ if (totalSeconds < 60) {
3705
+ return `${totalSeconds}s`;
3706
+ }
3707
+ const totalMinutes = Math.floor(totalSeconds / 60);
3708
+ if (totalMinutes < 60) {
3709
+ return `${totalMinutes}m`;
3710
+ }
3711
+ const totalHours = Math.floor(totalMinutes / 60);
3712
+ if (totalHours < 24) {
3713
+ return `${totalHours}h`;
3714
+ }
3715
+ return `${Math.floor(totalHours / 24)}d`;
3716
+ }
3717
+ function parseJsonObject(input) {
3718
+ try {
3719
+ const parsed = JSON.parse(input);
3720
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
3721
+ throw new Error('JSON payload must be an object.');
3722
+ }
3723
+ return parsed;
3724
+ }
3725
+ catch (error) {
3726
+ throw new Error(`JSON 解析失败: ${error instanceof Error ? error.message : String(error)}`);
3727
+ }
3728
+ }
3729
+ function clampListLimit(input, fallback, max) {
3730
+ const parsed = Number(input ?? fallback);
3731
+ if (!Number.isFinite(parsed) || parsed <= 0) {
3732
+ return fallback;
3733
+ }
3734
+ return Math.min(Math.trunc(parsed), max);
3735
+ }
3736
+ function mapRunStatusToPhase(status) {
3737
+ switch (status) {
3738
+ case 'queued':
3739
+ return '排队中';
3740
+ case 'running':
3741
+ return '执行中';
3742
+ case 'success':
3743
+ return '已完成';
3744
+ case 'failure':
3745
+ return '失败';
3746
+ case 'cancelled':
3747
+ return '已取消';
3748
+ case 'stale':
3749
+ return '中断';
3750
+ case 'orphaned':
3751
+ return '恢复中';
3752
+ default:
3753
+ return status;
3754
+ }
3755
+ }
3756
+ function replaceObject(target, next) {
3757
+ for (const key of Object.keys(target)) {
3758
+ if (!(key in next)) {
3759
+ delete target[key];
3760
+ }
3761
+ }
3762
+ for (const [key, value] of Object.entries(next)) {
3763
+ target[key] = value;
3764
+ }
3765
+ }
3766
+ function replaceProjects(target, next) {
3767
+ for (const key of Object.keys(target)) {
3768
+ if (!(key in next)) {
3769
+ delete target[key];
3770
+ }
3771
+ }
3772
+ for (const [alias, project] of Object.entries(next)) {
3773
+ target[alias] = project;
3774
+ }
3775
+ }
3776
+ function createDeferred() {
3777
+ let resolve;
3778
+ let reject;
3779
+ const promise = new Promise((innerResolve, innerReject) => {
3780
+ resolve = innerResolve;
3781
+ reject = innerReject;
3782
+ });
3783
+ return { promise, resolve, reject };
3784
+ }
3785
+ //# sourceMappingURL=service.js.map