@actagent/feishu 2026.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/actagent.plugin.json +224 -0
- package/api.ts +33 -0
- package/channel-entry.ts +21 -0
- package/channel-plugin-api.ts +2 -0
- package/contract-api.ts +17 -0
- package/index.ts +83 -0
- package/legacy-state-migrations-api.ts +2 -0
- package/npm-shrinkwrap.json +539 -0
- package/package.json +64 -0
- package/runtime-api.ts +58 -0
- package/runtime-setter-api.ts +3 -0
- package/secret-contract-api.ts +6 -0
- package/security-contract-api.ts +2 -0
- package/session-key-api.ts +2 -0
- package/setup-api.ts +4 -0
- package/setup-entry.test.ts +33 -0
- package/setup-entry.ts +25 -0
- package/skills/feishu-doc/SKILL.md +211 -0
- package/skills/feishu-doc/references/block-types.md +103 -0
- package/skills/feishu-drive/SKILL.md +97 -0
- package/skills/feishu-perm/SKILL.md +119 -0
- package/skills/feishu-wiki/SKILL.md +113 -0
- package/src/accounts.test.ts +481 -0
- package/src/accounts.ts +380 -0
- package/src/agent-config.ts +22 -0
- package/src/app-registration.test.ts +62 -0
- package/src/app-registration.ts +355 -0
- package/src/approval-auth.test.ts +25 -0
- package/src/approval-auth.ts +26 -0
- package/src/async.test.ts +68 -0
- package/src/async.ts +109 -0
- package/src/audio-preflight.runtime.ts +10 -0
- package/src/bitable.test.ts +174 -0
- package/src/bitable.ts +781 -0
- package/src/bot-content.ts +488 -0
- package/src/bot-group-name.test.ts +148 -0
- package/src/bot-runtime-api.ts +13 -0
- package/src/bot-sender-name.test.ts +68 -0
- package/src/bot-sender-name.ts +137 -0
- package/src/bot.broadcast.test.ts +643 -0
- package/src/bot.card-action.test.ts +647 -0
- package/src/bot.checkBotMentioned.test.ts +266 -0
- package/src/bot.helpers.test.ts +136 -0
- package/src/bot.stripBotMention.test.ts +127 -0
- package/src/bot.test.ts +3817 -0
- package/src/bot.ts +1788 -0
- package/src/card-action.ts +515 -0
- package/src/card-interaction.test.ts +132 -0
- package/src/card-interaction.ts +160 -0
- package/src/card-test-helpers.ts +55 -0
- package/src/card-ux-approval.ts +66 -0
- package/src/card-ux-launcher.test.ts +126 -0
- package/src/card-ux-launcher.ts +136 -0
- package/src/card-ux-shared.ts +34 -0
- package/src/channel-runtime-api.ts +17 -0
- package/src/channel.runtime.ts +48 -0
- package/src/channel.test.ts +1337 -0
- package/src/channel.ts +1401 -0
- package/src/chat-schema.ts +30 -0
- package/src/chat.test.ts +295 -0
- package/src/chat.ts +198 -0
- package/src/client-timeout.ts +44 -0
- package/src/client.test.ts +463 -0
- package/src/client.ts +263 -0
- package/src/comment-dispatcher-runtime-api.ts +7 -0
- package/src/comment-dispatcher.test.ts +186 -0
- package/src/comment-dispatcher.ts +108 -0
- package/src/comment-handler-runtime-api.ts +4 -0
- package/src/comment-handler.test.ts +588 -0
- package/src/comment-handler.ts +304 -0
- package/src/comment-reaction.test.ts +139 -0
- package/src/comment-reaction.ts +260 -0
- package/src/comment-shared.test.ts +184 -0
- package/src/comment-shared.ts +405 -0
- package/src/comment-target.ts +45 -0
- package/src/config-schema.test.ts +327 -0
- package/src/config-schema.ts +338 -0
- package/src/conversation-id.test.ts +19 -0
- package/src/conversation-id.ts +199 -0
- package/src/dedup-migrations.test.ts +90 -0
- package/src/dedup-migrations.ts +103 -0
- package/src/dedup.test.ts +95 -0
- package/src/dedup.ts +304 -0
- package/src/dedupe-key.ts +68 -0
- package/src/directory.static.ts +62 -0
- package/src/directory.test.ts +142 -0
- package/src/directory.ts +125 -0
- package/src/doc-schema.ts +183 -0
- package/src/doctor.test.ts +382 -0
- package/src/doctor.ts +876 -0
- package/src/docx-batch-insert.test.ts +117 -0
- package/src/docx-batch-insert.ts +223 -0
- package/src/docx-color-text.ts +154 -0
- package/src/docx-table-ops.test.ts +54 -0
- package/src/docx-table-ops.ts +316 -0
- package/src/docx-types.ts +39 -0
- package/src/docx.account-selection.test.ts +96 -0
- package/src/docx.test.ts +706 -0
- package/src/docx.ts +1598 -0
- package/src/drive-schema.ts +93 -0
- package/src/drive.test.ts +1240 -0
- package/src/drive.ts +830 -0
- package/src/dynamic-agent.test.ts +156 -0
- package/src/dynamic-agent.ts +144 -0
- package/src/event-types.ts +46 -0
- package/src/external-keys.test.ts +21 -0
- package/src/external-keys.ts +20 -0
- package/src/lifecycle.test-support.ts +223 -0
- package/src/media.test.ts +956 -0
- package/src/media.ts +1106 -0
- package/src/mention-target.types.ts +6 -0
- package/src/mention.ts +115 -0
- package/src/message-action-contract.ts +14 -0
- package/src/monitor-state-runtime-api.ts +8 -0
- package/src/monitor-transport-runtime-api.ts +11 -0
- package/src/monitor.account.ts +501 -0
- package/src/monitor.acp-init-failure.lifecycle.test-support.ts +215 -0
- package/src/monitor.bot-identity.ts +87 -0
- package/src/monitor.bot-menu-handler.ts +164 -0
- package/src/monitor.bot-menu.lifecycle.test-support.ts +221 -0
- package/src/monitor.bot-menu.test.ts +200 -0
- package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +265 -0
- package/src/monitor.card-action.lifecycle.test-support.ts +418 -0
- package/src/monitor.cleanup.test.ts +384 -0
- package/src/monitor.comment-notice-handler.ts +106 -0
- package/src/monitor.comment.test.ts +968 -0
- package/src/monitor.comment.ts +1386 -0
- package/src/monitor.lifecycle.test.ts +5 -0
- package/src/monitor.message-handler.ts +346 -0
- package/src/monitor.reaction.test.ts +770 -0
- package/src/monitor.startup.test.ts +232 -0
- package/src/monitor.startup.ts +76 -0
- package/src/monitor.state.defaults.test.ts +47 -0
- package/src/monitor.state.ts +171 -0
- package/src/monitor.synthetic-error.ts +19 -0
- package/src/monitor.test-mocks.ts +47 -0
- package/src/monitor.transport.ts +451 -0
- package/src/monitor.ts +104 -0
- package/src/monitor.webhook-e2e.test.ts +284 -0
- package/src/monitor.webhook-security.test.ts +394 -0
- package/src/monitor.webhook.test-helpers.ts +138 -0
- package/src/outbound-runtime-api.ts +2 -0
- package/src/outbound.test.ts +1255 -0
- package/src/outbound.ts +742 -0
- package/src/perm-schema.ts +53 -0
- package/src/perm.ts +171 -0
- package/src/pins.ts +109 -0
- package/src/policy.test.ts +224 -0
- package/src/policy.ts +322 -0
- package/src/post.test.ts +106 -0
- package/src/post.ts +276 -0
- package/src/presentation-card.ts +204 -0
- package/src/probe.test.ts +310 -0
- package/src/probe.ts +181 -0
- package/src/processing-claims.ts +60 -0
- package/src/qr-terminal.ts +2 -0
- package/src/reactions.ts +124 -0
- package/src/reasoning-preview.test.ts +114 -0
- package/src/reasoning-preview.ts +29 -0
- package/src/reply-dispatcher-runtime-api.ts +8 -0
- package/src/reply-dispatcher.test.ts +2009 -0
- package/src/reply-dispatcher.ts +865 -0
- package/src/runtime.ts +10 -0
- package/src/secret-contract.ts +146 -0
- package/src/secret-input.ts +2 -0
- package/src/security-audit-shared.ts +70 -0
- package/src/security-audit.test.ts +60 -0
- package/src/security-audit.ts +2 -0
- package/src/send-result.ts +81 -0
- package/src/send-target.test.ts +87 -0
- package/src/send-target.ts +36 -0
- package/src/send.reply-fallback.test.ts +418 -0
- package/src/send.test.ts +661 -0
- package/src/send.ts +860 -0
- package/src/sequential-key.test.ts +73 -0
- package/src/sequential-key.ts +29 -0
- package/src/sequential-queue.test.ts +184 -0
- package/src/sequential-queue.ts +90 -0
- package/src/session-conversation.ts +42 -0
- package/src/session-route.ts +49 -0
- package/src/setup-core.ts +52 -0
- package/src/setup-surface.test.ts +485 -0
- package/src/setup-surface.ts +620 -0
- package/src/streaming-card.test.ts +549 -0
- package/src/streaming-card.ts +611 -0
- package/src/subagent-hooks.test.ts +632 -0
- package/src/subagent-hooks.ts +414 -0
- package/src/targets.ts +98 -0
- package/src/test-support/lifecycle-test-support.ts +459 -0
- package/src/thread-bindings.test.ts +181 -0
- package/src/thread-bindings.ts +332 -0
- package/src/tool-account-routing.test.ts +419 -0
- package/src/tool-account.test.ts +45 -0
- package/src/tool-account.ts +98 -0
- package/src/tool-factory-test-harness.ts +83 -0
- package/src/tool-result.test.ts +33 -0
- package/src/tool-result.ts +17 -0
- package/src/tools-config.test.ts +52 -0
- package/src/tools-config.ts +29 -0
- package/src/types.ts +111 -0
- package/src/typing.test.ts +145 -0
- package/src/typing.ts +215 -0
- package/src/wiki-schema.ts +70 -0
- package/src/wiki.ts +271 -0
- package/subagent-hooks-api.ts +22 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// Feishu helper module supports doc schema behavior.
|
|
2
|
+
import { Type, type Static } from "typebox";
|
|
3
|
+
|
|
4
|
+
const tableCreationProperties = {
|
|
5
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
6
|
+
parent_block_id: Type.Optional(
|
|
7
|
+
Type.String({ description: "Parent block ID (default: document root)" }),
|
|
8
|
+
),
|
|
9
|
+
row_size: Type.Integer({ description: "Table row count", minimum: 1 }),
|
|
10
|
+
column_size: Type.Integer({ description: "Table column count", minimum: 1 }),
|
|
11
|
+
column_width: Type.Optional(
|
|
12
|
+
Type.Array(Type.Number({ minimum: 1 }), {
|
|
13
|
+
description: "Column widths in px (length should match column_size)",
|
|
14
|
+
}),
|
|
15
|
+
),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const FeishuDocSchema = Type.Union([
|
|
19
|
+
Type.Object({
|
|
20
|
+
action: Type.Literal("read"),
|
|
21
|
+
doc_token: Type.String({ description: "Document token (extract from URL /docx/XXX)" }),
|
|
22
|
+
}),
|
|
23
|
+
Type.Object({
|
|
24
|
+
action: Type.Literal("write"),
|
|
25
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
26
|
+
content: Type.String({
|
|
27
|
+
description: "Markdown content to write (replaces entire document content)",
|
|
28
|
+
}),
|
|
29
|
+
}),
|
|
30
|
+
Type.Object({
|
|
31
|
+
action: Type.Literal("append"),
|
|
32
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
33
|
+
content: Type.String({ description: "Markdown content to append to end of document" }),
|
|
34
|
+
}),
|
|
35
|
+
Type.Object({
|
|
36
|
+
action: Type.Literal("insert"),
|
|
37
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
38
|
+
content: Type.String({ description: "Markdown content to insert" }),
|
|
39
|
+
after_block_id: Type.String({
|
|
40
|
+
description: "Insert content after this block ID. Use list_blocks to find block IDs.",
|
|
41
|
+
}),
|
|
42
|
+
}),
|
|
43
|
+
Type.Object({
|
|
44
|
+
action: Type.Literal("create"),
|
|
45
|
+
title: Type.String({ description: "Document title" }),
|
|
46
|
+
folder_token: Type.Optional(Type.String({ description: "Target folder token (optional)" })),
|
|
47
|
+
grant_to_requester: Type.Optional(
|
|
48
|
+
Type.Boolean({
|
|
49
|
+
description:
|
|
50
|
+
"Grant edit permission to the trusted requesting Feishu user from runtime context (default: true).",
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
}),
|
|
54
|
+
Type.Object({
|
|
55
|
+
action: Type.Literal("list_blocks"),
|
|
56
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
57
|
+
}),
|
|
58
|
+
Type.Object({
|
|
59
|
+
action: Type.Literal("get_block"),
|
|
60
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
61
|
+
block_id: Type.String({ description: "Block ID (from list_blocks)" }),
|
|
62
|
+
}),
|
|
63
|
+
Type.Object({
|
|
64
|
+
action: Type.Literal("update_block"),
|
|
65
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
66
|
+
block_id: Type.String({ description: "Block ID (from list_blocks)" }),
|
|
67
|
+
content: Type.String({ description: "New text content" }),
|
|
68
|
+
}),
|
|
69
|
+
Type.Object({
|
|
70
|
+
action: Type.Literal("delete_block"),
|
|
71
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
72
|
+
block_id: Type.String({ description: "Block ID" }),
|
|
73
|
+
}),
|
|
74
|
+
// Table creation (explicit structure)
|
|
75
|
+
Type.Object({
|
|
76
|
+
action: Type.Literal("create_table"),
|
|
77
|
+
...tableCreationProperties,
|
|
78
|
+
}),
|
|
79
|
+
Type.Object({
|
|
80
|
+
action: Type.Literal("write_table_cells"),
|
|
81
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
82
|
+
table_block_id: Type.String({ description: "Table block ID" }),
|
|
83
|
+
values: Type.Array(Type.Array(Type.String()), {
|
|
84
|
+
description: "2D matrix values[row][col] to write into table cells",
|
|
85
|
+
minItems: 1,
|
|
86
|
+
}),
|
|
87
|
+
}),
|
|
88
|
+
Type.Object({
|
|
89
|
+
action: Type.Literal("create_table_with_values"),
|
|
90
|
+
...tableCreationProperties,
|
|
91
|
+
values: Type.Array(Type.Array(Type.String()), {
|
|
92
|
+
description: "2D matrix values[row][col] to write into table cells",
|
|
93
|
+
minItems: 1,
|
|
94
|
+
}),
|
|
95
|
+
}),
|
|
96
|
+
// Table row/column manipulation
|
|
97
|
+
Type.Object({
|
|
98
|
+
action: Type.Literal("insert_table_row"),
|
|
99
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
100
|
+
block_id: Type.String({ description: "Table block ID" }),
|
|
101
|
+
row_index: Type.Optional(
|
|
102
|
+
Type.Number({ description: "Row index to insert at (-1 for end, default: -1)" }),
|
|
103
|
+
),
|
|
104
|
+
}),
|
|
105
|
+
Type.Object({
|
|
106
|
+
action: Type.Literal("insert_table_column"),
|
|
107
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
108
|
+
block_id: Type.String({ description: "Table block ID" }),
|
|
109
|
+
column_index: Type.Optional(
|
|
110
|
+
Type.Number({ description: "Column index to insert at (-1 for end, default: -1)" }),
|
|
111
|
+
),
|
|
112
|
+
}),
|
|
113
|
+
Type.Object({
|
|
114
|
+
action: Type.Literal("delete_table_rows"),
|
|
115
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
116
|
+
block_id: Type.String({ description: "Table block ID" }),
|
|
117
|
+
row_start: Type.Number({ description: "Start row index (0-based)" }),
|
|
118
|
+
row_count: Type.Optional(Type.Number({ description: "Number of rows to delete (default: 1)" })),
|
|
119
|
+
}),
|
|
120
|
+
Type.Object({
|
|
121
|
+
action: Type.Literal("delete_table_columns"),
|
|
122
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
123
|
+
block_id: Type.String({ description: "Table block ID" }),
|
|
124
|
+
column_start: Type.Number({ description: "Start column index (0-based)" }),
|
|
125
|
+
column_count: Type.Optional(
|
|
126
|
+
Type.Number({ description: "Number of columns to delete (default: 1)" }),
|
|
127
|
+
),
|
|
128
|
+
}),
|
|
129
|
+
Type.Object({
|
|
130
|
+
action: Type.Literal("merge_table_cells"),
|
|
131
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
132
|
+
block_id: Type.String({ description: "Table block ID" }),
|
|
133
|
+
row_start: Type.Number({ description: "Start row index" }),
|
|
134
|
+
row_end: Type.Number({ description: "End row index (exclusive)" }),
|
|
135
|
+
column_start: Type.Number({ description: "Start column index" }),
|
|
136
|
+
column_end: Type.Number({ description: "End column index (exclusive)" }),
|
|
137
|
+
}),
|
|
138
|
+
// Image / file upload
|
|
139
|
+
Type.Object({
|
|
140
|
+
action: Type.Literal("upload_image"),
|
|
141
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
142
|
+
url: Type.Optional(Type.String({ description: "Remote image URL (http/https)" })),
|
|
143
|
+
file_path: Type.Optional(Type.String({ description: "Local image file path" })),
|
|
144
|
+
image: Type.Optional(
|
|
145
|
+
Type.String({
|
|
146
|
+
description:
|
|
147
|
+
"Image as data URI (data:image/png;base64,...) or plain base64 string. Use instead of url/file_path for DALL-E outputs, canvas screenshots, etc.",
|
|
148
|
+
}),
|
|
149
|
+
),
|
|
150
|
+
parent_block_id: Type.Optional(
|
|
151
|
+
Type.String({ description: "Parent block ID (default: document root)" }),
|
|
152
|
+
),
|
|
153
|
+
filename: Type.Optional(Type.String({ description: "Optional filename override" })),
|
|
154
|
+
index: Type.Optional(
|
|
155
|
+
Type.Integer({
|
|
156
|
+
minimum: 0,
|
|
157
|
+
description: "Insert position (0-based index among siblings). Omit to append.",
|
|
158
|
+
}),
|
|
159
|
+
),
|
|
160
|
+
}),
|
|
161
|
+
Type.Object({
|
|
162
|
+
action: Type.Literal("upload_file"),
|
|
163
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
164
|
+
url: Type.Optional(Type.String({ description: "Remote file URL (http/https)" })),
|
|
165
|
+
file_path: Type.Optional(Type.String({ description: "Local file path" })),
|
|
166
|
+
parent_block_id: Type.Optional(
|
|
167
|
+
Type.String({ description: "Parent block ID (default: document root)" }),
|
|
168
|
+
),
|
|
169
|
+
filename: Type.Optional(Type.String({ description: "Optional filename override" })),
|
|
170
|
+
}),
|
|
171
|
+
// Text color / style
|
|
172
|
+
Type.Object({
|
|
173
|
+
action: Type.Literal("color_text"),
|
|
174
|
+
doc_token: Type.String({ description: "Document token" }),
|
|
175
|
+
block_id: Type.String({ description: "Text block ID to update" }),
|
|
176
|
+
content: Type.String({
|
|
177
|
+
description:
|
|
178
|
+
'Text with color markup. Tags: [red], [green], [blue], [orange], [yellow], [purple], [grey], [bold], [bg:yellow]. Example: "Revenue [green]+15%[/green] YoY"',
|
|
179
|
+
}),
|
|
180
|
+
}),
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
export type FeishuDocParams = Static<typeof FeishuDocSchema>;
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// Feishu tests cover doctor plugin behavior.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { loadSessionStore } from "actagent/plugin-sdk/session-store-runtime";
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
7
|
+
import type { ACTAgentConfig } from "../runtime-api.js";
|
|
8
|
+
import { isFeishuSessionStoreKey, runFeishuDoctorSequence } from "./doctor.js";
|
|
9
|
+
|
|
10
|
+
type EnvSnapshot = {
|
|
11
|
+
HOME?: string;
|
|
12
|
+
ACTAGENT_HOME?: string;
|
|
13
|
+
ACTAGENT_STATE_DIR?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function captureEnv(): EnvSnapshot {
|
|
17
|
+
return {
|
|
18
|
+
HOME: process.env.HOME,
|
|
19
|
+
ACTAGENT_HOME: process.env.ACTAGENT_HOME,
|
|
20
|
+
ACTAGENT_STATE_DIR: process.env.ACTAGENT_STATE_DIR,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function restoreEnv(snapshot: EnvSnapshot) {
|
|
25
|
+
for (const key of Object.keys(snapshot) as Array<keyof EnvSnapshot>) {
|
|
26
|
+
const value = snapshot[key];
|
|
27
|
+
if (value === undefined) {
|
|
28
|
+
delete process.env[key];
|
|
29
|
+
} else {
|
|
30
|
+
process.env[key] = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function feishuConfig(): ACTAgentConfig {
|
|
36
|
+
return {
|
|
37
|
+
channels: {
|
|
38
|
+
feishu: {
|
|
39
|
+
appId: "cli_xxx",
|
|
40
|
+
appSecret: "secret_xxx",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
} as ACTAgentConfig;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function stateDir(): string {
|
|
47
|
+
const dir = process.env.ACTAGENT_STATE_DIR;
|
|
48
|
+
if (!dir) {
|
|
49
|
+
throw new Error("ACTAGENT_STATE_DIR is not set");
|
|
50
|
+
}
|
|
51
|
+
return dir;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sessionsDir(agentId = "main"): string {
|
|
55
|
+
return path.join(stateDir(), "agents", agentId, "sessions");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function storePath(agentId = "main"): string {
|
|
59
|
+
return path.join(sessionsDir(agentId), "sessions.json");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function writeStore(entries: Record<string, unknown>, agentId = "main"): string {
|
|
63
|
+
const target = storePath(agentId);
|
|
64
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
65
|
+
fs.writeFileSync(target, JSON.stringify(entries, null, 2));
|
|
66
|
+
return target;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writeTranscript(sessionId: string, lines: unknown[], agentId = "main"): string {
|
|
70
|
+
const target = path.join(sessionsDir(agentId), `${sessionId}.jsonl`);
|
|
71
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
72
|
+
fs.writeFileSync(target, `${lines.map((line) => JSON.stringify(line)).join("\n")}\n`);
|
|
73
|
+
return target;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sessionHeader(sessionId: string) {
|
|
77
|
+
return {
|
|
78
|
+
type: "session",
|
|
79
|
+
id: sessionId,
|
|
80
|
+
version: 7,
|
|
81
|
+
timestamp: new Date(0).toISOString(),
|
|
82
|
+
cwd: "/tmp",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function userMessage(content: string) {
|
|
87
|
+
return {
|
|
88
|
+
type: "message",
|
|
89
|
+
id: `msg-${content || "blank"}-${Math.random().toString(36).slice(2)}`,
|
|
90
|
+
parentId: null,
|
|
91
|
+
timestamp: new Date(0).toISOString(),
|
|
92
|
+
message: { role: "user", content },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function listBackupDirs(): string[] {
|
|
97
|
+
const backupsDir = path.join(stateDir(), "backups");
|
|
98
|
+
return fs.existsSync(backupsDir)
|
|
99
|
+
? fs.readdirSync(backupsDir).filter((name) => name.startsWith("feishu-state-repair-"))
|
|
100
|
+
: [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
describe("Feishu doctor state repair", () => {
|
|
104
|
+
let envSnapshot: EnvSnapshot;
|
|
105
|
+
let tempHome = "";
|
|
106
|
+
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
envSnapshot = captureEnv();
|
|
109
|
+
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "actagent-feishu-doctor-"));
|
|
110
|
+
process.env.HOME = tempHome;
|
|
111
|
+
process.env.ACTAGENT_HOME = tempHome;
|
|
112
|
+
process.env.ACTAGENT_STATE_DIR = path.join(tempHome, ".actagent");
|
|
113
|
+
fs.mkdirSync(process.env.ACTAGENT_STATE_DIR, { recursive: true, mode: 0o700 });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
restoreEnv(envSnapshot);
|
|
118
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("matches only Feishu channel session keys", () => {
|
|
122
|
+
expect(isFeishuSessionStoreKey("agent:main:feishu:direct:ou_user")).toBe(true);
|
|
123
|
+
expect(isFeishuSessionStoreKey("feishu:direct:ou_user")).toBe(true);
|
|
124
|
+
expect(isFeishuSessionStoreKey("agent:codex:acp:binding:feishu:default:abc123")).toBe(false);
|
|
125
|
+
expect(isFeishuSessionStoreKey("agent:main:discord:direct:user")).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("stays quiet for healthy Feishu state and transcripts", async () => {
|
|
129
|
+
const feishuDedupDir = path.join(stateDir(), "feishu", "dedup");
|
|
130
|
+
fs.mkdirSync(feishuDedupDir, { recursive: true });
|
|
131
|
+
fs.writeFileSync(path.join(feishuDedupDir, "default.json"), JSON.stringify({ msg1: 1 }));
|
|
132
|
+
|
|
133
|
+
writeTranscript("sess-ok", [sessionHeader("sess-ok"), userMessage("hello")]);
|
|
134
|
+
writeStore({
|
|
135
|
+
"agent:main:feishu:direct:ou_user": {
|
|
136
|
+
sessionId: "sess-ok",
|
|
137
|
+
sessionFile: "sess-ok.jsonl",
|
|
138
|
+
updatedAt: Date.now(),
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const result = await runFeishuDoctorSequence({
|
|
143
|
+
cfg: feishuConfig(),
|
|
144
|
+
env: process.env,
|
|
145
|
+
shouldRepair: false,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(result).toEqual({ changeNotes: [], warningNotes: [] });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("keeps custom-store sessions with canonical absolute transcripts", async () => {
|
|
152
|
+
const transcriptPath = writeTranscript("sess-abs", [
|
|
153
|
+
sessionHeader("sess-abs"),
|
|
154
|
+
userMessage("hello"),
|
|
155
|
+
]);
|
|
156
|
+
const customStorePath = path.join(stateDir(), "custom-sessions", "sessions.json");
|
|
157
|
+
fs.mkdirSync(path.dirname(customStorePath), { recursive: true });
|
|
158
|
+
fs.writeFileSync(
|
|
159
|
+
customStorePath,
|
|
160
|
+
JSON.stringify({
|
|
161
|
+
"agent:main:feishu:direct:ou_user": {
|
|
162
|
+
sessionId: "sess-abs",
|
|
163
|
+
sessionFile: transcriptPath,
|
|
164
|
+
updatedAt: Date.now(),
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const result = await runFeishuDoctorSequence({
|
|
170
|
+
cfg: {
|
|
171
|
+
...feishuConfig(),
|
|
172
|
+
session: { store: customStorePath },
|
|
173
|
+
} as ACTAgentConfig,
|
|
174
|
+
env: process.env,
|
|
175
|
+
shouldRepair: false,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(result).toEqual({ changeNotes: [], warningNotes: [] });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("keeps Feishu sessions with separated blank user messages", async () => {
|
|
182
|
+
writeTranscript("sess-separated-blanks", [
|
|
183
|
+
sessionHeader("sess-separated-blanks"),
|
|
184
|
+
userMessage(""),
|
|
185
|
+
userMessage("hello"),
|
|
186
|
+
userMessage(""),
|
|
187
|
+
userMessage("world"),
|
|
188
|
+
userMessage(""),
|
|
189
|
+
]);
|
|
190
|
+
writeStore({
|
|
191
|
+
"agent:main:feishu:direct:ou_user": {
|
|
192
|
+
sessionId: "sess-separated-blanks",
|
|
193
|
+
sessionFile: "sess-separated-blanks.jsonl",
|
|
194
|
+
updatedAt: Date.now(),
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const result = await runFeishuDoctorSequence({
|
|
199
|
+
cfg: feishuConfig(),
|
|
200
|
+
env: process.env,
|
|
201
|
+
shouldRepair: false,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(result).toEqual({ changeNotes: [], warningNotes: [] });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("warns before repair when Feishu local state is corrupt", async () => {
|
|
208
|
+
const feishuDedupDir = path.join(stateDir(), "feishu", "dedup");
|
|
209
|
+
fs.mkdirSync(feishuDedupDir, { recursive: true });
|
|
210
|
+
fs.writeFileSync(path.join(feishuDedupDir, "default.json"), "{");
|
|
211
|
+
|
|
212
|
+
const result = await runFeishuDoctorSequence({
|
|
213
|
+
cfg: feishuConfig(),
|
|
214
|
+
env: process.env,
|
|
215
|
+
shouldRepair: false,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(result.changeNotes).toEqual([]);
|
|
219
|
+
expect(result.warningNotes.join("\n")).toContain("Feishu local channel state may need repair");
|
|
220
|
+
expect(result.warningNotes.join("\n")).toContain("preserving Feishu App ID/secret config");
|
|
221
|
+
expect(result.warningNotes.join("\n")).toContain("actagent doctor --fix");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("rebuilds corrupt Feishu state without deleting healthy Feishu sessions", async () => {
|
|
225
|
+
const feishuDedupDir = path.join(stateDir(), "feishu", "dedup");
|
|
226
|
+
fs.mkdirSync(feishuDedupDir, { recursive: true });
|
|
227
|
+
fs.writeFileSync(path.join(feishuDedupDir, "default.json"), "{");
|
|
228
|
+
|
|
229
|
+
const transcriptPath = writeTranscript("sess-ok", [
|
|
230
|
+
sessionHeader("sess-ok"),
|
|
231
|
+
userMessage("hello"),
|
|
232
|
+
]);
|
|
233
|
+
const targetStorePath = writeStore({
|
|
234
|
+
"agent:main:feishu:direct:ou_user": {
|
|
235
|
+
sessionId: "sess-ok",
|
|
236
|
+
sessionFile: "sess-ok.jsonl",
|
|
237
|
+
updatedAt: Date.now(),
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const result = await runFeishuDoctorSequence({
|
|
242
|
+
cfg: feishuConfig(),
|
|
243
|
+
env: process.env,
|
|
244
|
+
shouldRepair: true,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(result.warningNotes).toEqual([]);
|
|
248
|
+
expect(result.changeNotes.join("\n")).toContain("Rebuilt Feishu runtime state: yes");
|
|
249
|
+
expect(result.changeNotes.join("\n")).toContain("Removed 0 Feishu-scoped session entries");
|
|
250
|
+
|
|
251
|
+
const store = loadSessionStore(targetStorePath, { skipCache: true });
|
|
252
|
+
expect(store["agent:main:feishu:direct:ou_user"]).toBeDefined();
|
|
253
|
+
expect(fs.existsSync(transcriptPath)).toBe(true);
|
|
254
|
+
|
|
255
|
+
expect(fs.existsSync(path.join(stateDir(), "feishu"))).toBe(true);
|
|
256
|
+
expect(fs.existsSync(path.join(stateDir(), "feishu", "dedup", "default.json"))).toBe(false);
|
|
257
|
+
|
|
258
|
+
const backups = listBackupDirs();
|
|
259
|
+
expect(backups).toHaveLength(1);
|
|
260
|
+
const backupDir = path.join(stateDir(), "backups", backups[0] ?? "");
|
|
261
|
+
expect(fs.existsSync(path.join(backupDir, "feishu", "dedup", "default.json"))).toBe(true);
|
|
262
|
+
expect(fs.existsSync(path.join(backupDir, "session-stores", "main", "sessions.json"))).toBe(
|
|
263
|
+
false,
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("archives only unhealthy Feishu direct sessions while preserving state, config, and other sessions", async () => {
|
|
268
|
+
const feishuDedupDir = path.join(stateDir(), "feishu", "dedup");
|
|
269
|
+
fs.mkdirSync(feishuDedupDir, { recursive: true });
|
|
270
|
+
fs.writeFileSync(path.join(feishuDedupDir, "default.json"), JSON.stringify({ msg1: 1 }));
|
|
271
|
+
|
|
272
|
+
const transcriptPath = writeTranscript("sess-bad", [
|
|
273
|
+
sessionHeader("sess-bad"),
|
|
274
|
+
userMessage(""),
|
|
275
|
+
userMessage(""),
|
|
276
|
+
userMessage(""),
|
|
277
|
+
]);
|
|
278
|
+
const trajectoryPath = path.join(sessionsDir(), "sess-bad.trajectory.jsonl");
|
|
279
|
+
const trajectoryIndexPath = path.join(sessionsDir(), "sess-bad.trajectory-path.json");
|
|
280
|
+
fs.writeFileSync(trajectoryPath, "{}\n");
|
|
281
|
+
fs.writeFileSync(trajectoryIndexPath, "{}\n");
|
|
282
|
+
const acpTranscriptPath = writeTranscript("sess-acp-bad", [
|
|
283
|
+
sessionHeader("sess-acp-bad"),
|
|
284
|
+
userMessage(""),
|
|
285
|
+
userMessage(""),
|
|
286
|
+
userMessage(""),
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
const targetStorePath = writeStore({
|
|
290
|
+
"agent:main:feishu:direct:ou_user": {
|
|
291
|
+
sessionId: "sess-bad",
|
|
292
|
+
sessionFile: "sess-bad.jsonl",
|
|
293
|
+
updatedAt: Date.now(),
|
|
294
|
+
},
|
|
295
|
+
"agent:codex:acp:binding:feishu:default:abc123": {
|
|
296
|
+
sessionId: "sess-acp-bad",
|
|
297
|
+
sessionFile: "sess-acp-bad.jsonl",
|
|
298
|
+
updatedAt: Date.now(),
|
|
299
|
+
route: { channel: "feishu", target: { to: "ou_user", chatType: "direct" } },
|
|
300
|
+
},
|
|
301
|
+
"agent:main:discord:direct:user": {
|
|
302
|
+
sessionId: "sess-discord",
|
|
303
|
+
updatedAt: Date.now(),
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const result = await runFeishuDoctorSequence({
|
|
308
|
+
cfg: feishuConfig(),
|
|
309
|
+
env: process.env,
|
|
310
|
+
shouldRepair: true,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
expect(result.warningNotes).toEqual([]);
|
|
314
|
+
expect(result.changeNotes.join("\n")).toContain("Feishu local state repaired");
|
|
315
|
+
expect(result.changeNotes.join("\n")).toContain("Rebuilt Feishu runtime state: not needed");
|
|
316
|
+
expect(result.changeNotes.join("\n")).toContain("Preserved Feishu App ID/secret config");
|
|
317
|
+
|
|
318
|
+
expect(fs.existsSync(path.join(stateDir(), "feishu"))).toBe(true);
|
|
319
|
+
expect(fs.existsSync(path.join(stateDir(), "feishu", "dedup", "default.json"))).toBe(true);
|
|
320
|
+
|
|
321
|
+
const backups = listBackupDirs();
|
|
322
|
+
expect(backups).toHaveLength(1);
|
|
323
|
+
const backupDir = path.join(stateDir(), "backups", backups[0] ?? "");
|
|
324
|
+
expect(fs.existsSync(path.join(backupDir, "feishu", "dedup", "default.json"))).toBe(false);
|
|
325
|
+
expect(fs.existsSync(path.join(backupDir, "session-stores", "main", "sessions.json"))).toBe(
|
|
326
|
+
true,
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const store = loadSessionStore(targetStorePath, { skipCache: true });
|
|
330
|
+
expect(store["agent:main:feishu:direct:ou_user"]).toBeUndefined();
|
|
331
|
+
expect(store["agent:codex:acp:binding:feishu:default:abc123"]).toBeDefined();
|
|
332
|
+
expect(store["agent:main:discord:direct:user"]).toBeDefined();
|
|
333
|
+
|
|
334
|
+
expect(fs.existsSync(transcriptPath)).toBe(false);
|
|
335
|
+
expect(fs.existsSync(acpTranscriptPath)).toBe(true);
|
|
336
|
+
expect(fs.existsSync(trajectoryPath)).toBe(false);
|
|
337
|
+
expect(fs.existsSync(trajectoryIndexPath)).toBe(false);
|
|
338
|
+
const archivedNames = fs.readdirSync(sessionsDir());
|
|
339
|
+
expect(archivedNames.some((name) => name.startsWith("sess-bad.jsonl.deleted."))).toBe(true);
|
|
340
|
+
expect(
|
|
341
|
+
archivedNames.some((name) => name.startsWith("sess-bad.trajectory.jsonl.deleted.")),
|
|
342
|
+
).toBe(true);
|
|
343
|
+
expect(
|
|
344
|
+
archivedNames.some((name) => name.startsWith("sess-bad.trajectory-path.json.deleted.")),
|
|
345
|
+
).toBe(true);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("archives unhealthy default-scope sessions when metadata identifies Feishu", async () => {
|
|
349
|
+
const transcriptPath = writeTranscript("sess-default-feishu-bad", [
|
|
350
|
+
sessionHeader("sess-default-feishu-bad"),
|
|
351
|
+
userMessage(""),
|
|
352
|
+
userMessage(""),
|
|
353
|
+
userMessage(""),
|
|
354
|
+
]);
|
|
355
|
+
const targetStorePath = writeStore({
|
|
356
|
+
"agent:main:main": {
|
|
357
|
+
sessionId: "sess-default-feishu-bad",
|
|
358
|
+
sessionFile: "sess-default-feishu-bad.jsonl",
|
|
359
|
+
updatedAt: Date.now(),
|
|
360
|
+
origin: { provider: "feishu", from: "feishu:ou_user" },
|
|
361
|
+
route: { channel: "feishu", target: { to: "ou_user", chatType: "direct" } },
|
|
362
|
+
},
|
|
363
|
+
"agent:main:main-non-feishu": {
|
|
364
|
+
sessionId: "sess-other",
|
|
365
|
+
updatedAt: Date.now(),
|
|
366
|
+
origin: { provider: "discord" },
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const result = await runFeishuDoctorSequence({
|
|
371
|
+
cfg: feishuConfig(),
|
|
372
|
+
env: process.env,
|
|
373
|
+
shouldRepair: true,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
expect(result.warningNotes).toEqual([]);
|
|
377
|
+
const store = loadSessionStore(targetStorePath, { skipCache: true });
|
|
378
|
+
expect(store["agent:main:main"]).toBeUndefined();
|
|
379
|
+
expect(store["agent:main:main-non-feishu"]).toBeDefined();
|
|
380
|
+
expect(fs.existsSync(transcriptPath)).toBe(false);
|
|
381
|
+
});
|
|
382
|
+
});
|