@foxden-app/foxclaw 0.12.0 → 0.13.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.
- package/CHANGELOG.md +20 -0
- package/dist/antigravity/account_ui.d.ts +34 -0
- package/dist/antigravity/account_ui.js +354 -0
- package/dist/antigravity/adapter.js +1 -1
- package/dist/antigravity/auth.d.ts +4 -1
- package/dist/antigravity/auth.js +17 -5
- package/dist/antigravity/controller.d.ts +3 -108
- package/dist/antigravity/controller.js +2 -2129
- package/dist/antigravity/conversation_ui.d.ts +56 -0
- package/dist/antigravity/conversation_ui.js +509 -0
- package/dist/antigravity/ui_callbacks.d.ts +6 -0
- package/dist/antigravity/ui_callbacks.js +6 -0
- package/dist/bridge/unified_bridge.d.ts +114 -0
- package/dist/bridge/unified_bridge.js +1343 -0
- package/dist/channels/bridge_messaging_router.d.ts +8 -4
- package/dist/channels/bridge_messaging_router.js +15 -4
- package/dist/channels/telegram/telegram_messaging_port.d.ts +16 -0
- package/dist/channels/telegram/telegram_messaging_port.js +22 -2
- package/dist/channels/text_actions.d.ts +14 -0
- package/dist/channels/text_actions.js +48 -0
- package/dist/channels/weixin/weixin_channel_adapter.d.ts +3 -4
- package/dist/channels/weixin/weixin_channel_adapter.js +2 -2
- package/dist/channels/weixin/weixin_messaging_port.d.ts +16 -1
- package/dist/channels/weixin/weixin_messaging_port.js +34 -7
- package/dist/codex_app/activity.d.ts +62 -0
- package/dist/codex_app/activity.js +418 -0
- package/dist/codex_app/adapter.d.ts +2 -2
- package/dist/codex_app/adapter.js +55 -25
- package/dist/codex_app/backend_ui.d.ts +4 -0
- package/dist/codex_app/backend_ui.js +48 -0
- package/dist/codex_app/client.js +0 -12
- package/dist/controller/access.d.ts +1 -11
- package/dist/controller/access.js +1 -33
- package/dist/controller/activity.d.ts +1 -62
- package/dist/controller/activity.js +2 -418
- package/dist/controller/auth_errors.d.ts +11 -0
- package/dist/controller/auth_errors.js +124 -0
- package/dist/controller/auth_files.d.ts +13 -0
- package/dist/controller/auth_files.js +156 -0
- package/dist/controller/auth_presentation.d.ts +77 -0
- package/dist/controller/auth_presentation.js +697 -0
- package/dist/controller/auth_quota_service.d.ts +27 -0
- package/dist/controller/auth_quota_service.js +151 -0
- package/dist/controller/controller.d.ts +30 -56
- package/dist/controller/controller.js +181 -3497
- package/dist/controller/interactions.d.ts +74 -0
- package/dist/controller/interactions.js +560 -0
- package/dist/controller/local_usage_service.d.ts +19 -0
- package/dist/controller/local_usage_service.js +62 -0
- package/dist/controller/native_panel_service.d.ts +55 -0
- package/dist/controller/native_panel_service.js +214 -0
- package/dist/controller/native_panels.d.ts +61 -0
- package/dist/controller/native_panels.js +784 -0
- package/dist/controller/shared_helpers.d.ts +53 -0
- package/dist/controller/shared_helpers.js +399 -0
- package/dist/controller/state_types.d.ts +390 -0
- package/dist/controller/state_types.js +82 -0
- package/dist/controller/turn_rendering.d.ts +22 -0
- package/dist/controller/turn_rendering.js +288 -0
- package/dist/core/access.d.ts +11 -0
- package/dist/core/access.js +33 -0
- package/dist/core/attachment_files.d.ts +13 -0
- package/dist/core/attachment_files.js +180 -0
- package/dist/core/attachment_types.d.ts +22 -0
- package/dist/core/attachment_types.js +1 -0
- package/dist/core/attachments.d.ts +5 -3
- package/dist/core/attachments.js +2 -2
- package/dist/core/backend_registry.d.ts +22 -0
- package/dist/core/backend_registry.js +50 -0
- package/dist/core/backend_ui.d.ts +28 -0
- package/dist/core/backend_ui.js +1 -0
- package/dist/core/channel_events.d.ts +48 -0
- package/dist/core/channel_events.js +1 -0
- package/dist/core/channel_port.d.ts +52 -8
- package/dist/core/engine_spi.d.ts +19 -4
- package/dist/core/listener_scope.d.ts +11 -0
- package/dist/core/listener_scope.js +10 -0
- package/dist/core/message_text.d.ts +2 -0
- package/dist/core/message_text.js +28 -0
- package/dist/core/network_errors.d.ts +1 -0
- package/dist/core/network_errors.js +23 -0
- package/dist/core/orchestrator.d.ts +53 -24
- package/dist/core/orchestrator.js +855 -507
- package/dist/core/queued_engine_input.d.ts +12 -0
- package/dist/core/queued_engine_input.js +16 -0
- package/dist/core/runtime_supervisor.d.ts +19 -0
- package/dist/core/runtime_supervisor.js +45 -0
- package/dist/core/scope_operations.d.ts +7 -0
- package/dist/core/scope_operations.js +19 -0
- package/dist/core/stream_preview.js +21 -9
- package/dist/dsh/adapter.js +18 -5
- package/dist/dsh/backend.d.ts +7 -0
- package/dist/dsh/backend.js +21 -0
- package/dist/dsh/runtime.d.ts +0 -1
- package/dist/dsh/runtime.js +4 -13
- package/dist/dsh/ui.d.ts +8 -8
- package/dist/dsh/ui.js +1 -1
- package/dist/main.js +55 -43
- package/dist/opencode/adapter.js +28 -9
- package/dist/opencode/controller.js +1 -1
- package/dist/store/channel_inbox.d.ts +14 -0
- package/dist/store/channel_inbox.js +21 -0
- package/dist/store/database.d.ts +7 -3
- package/dist/store/database.js +7 -2
- package/dist/store/task_journal.d.ts +32 -0
- package/dist/store/task_journal.js +70 -0
- package/dist/telegram/api.d.ts +2 -2
- package/dist/telegram/api.js +10 -28
- package/dist/telegram/gateway.d.ts +13 -27
- package/dist/telegram/gateway.js +86 -43
- package/dist/telegram/media.d.ts +2 -34
- package/dist/telegram/media.js +1 -180
- package/dist/telegram/task_preview.d.ts +13 -0
- package/dist/telegram/task_preview.js +92 -0
- package/dist/telegram/task_result.d.ts +2 -0
- package/dist/telegram/task_result.js +12 -0
- package/dist/telegram/text.d.ts +1 -1
- package/dist/telegram/text.js +2 -27
- package/dist/types.d.ts +1 -1
- package/docs/release.md +13 -11
- package/docs/zh/architecture-review.md +106 -0
- package/docs/zh/release.md +13 -11
- package/docs/zh/telegram-experience.md +40 -0
- package/package.json +5 -4
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { AppLocale, AppTurnSnapshot, PendingAttachmentBatchRecord, PendingApprovalRecord } from '../types.js';
|
|
2
|
+
import type { ActiveTurnPreviewRecord, PendingUserInputStoredRecord } from '../store/database.js';
|
|
3
|
+
import { type ObservedTurnCursor } from './observer.js';
|
|
4
|
+
import type { TurnInput } from '../codex_app/client.js';
|
|
5
|
+
import { type StagedTelegramAttachment } from '../telegram/media.js';
|
|
6
|
+
import { PendingMcpElicitation, McpElicitationAction, ActiveTurn, ApprovalAction, PendingUserInputQuestion, ServerRequestId, PendingUserInputRequest, PendingUserInputStatus, PendingPlanImplementation } from './state_types.js';
|
|
7
|
+
export declare function renderMcpElicitationMessage(locale: AppLocale, record: PendingMcpElicitation, decision?: McpElicitationAction): string;
|
|
8
|
+
export declare function mcpElicitationKeyboard(locale: AppLocale, record: PendingMcpElicitation): Array<Array<{
|
|
9
|
+
text: string;
|
|
10
|
+
callback_data: string;
|
|
11
|
+
}>>;
|
|
12
|
+
export declare function formatPermissionRequestSummary(locale: AppLocale, permissions: any): string;
|
|
13
|
+
export declare function resolveScopeMessageTarget(scopeId: string): {
|
|
14
|
+
chatId: string;
|
|
15
|
+
chatType: string;
|
|
16
|
+
topicId: number | null;
|
|
17
|
+
} | null;
|
|
18
|
+
export declare function restartPreviewRecoveryKey(preview: Pick<ActiveTurnPreviewRecord, 'scopeId' | 'turnId' | 'messageId'>): string;
|
|
19
|
+
export declare function seedObservedTurnCursor(turn: AppTurnSnapshot): ObservedTurnCursor;
|
|
20
|
+
export declare function observerCursorFromActiveTurn(active: ActiveTurn): ObservedTurnCursor;
|
|
21
|
+
export declare function approvalKeyboard(locale: AppLocale, localId: string): Array<Array<{
|
|
22
|
+
text: string;
|
|
23
|
+
callback_data: string;
|
|
24
|
+
}>>;
|
|
25
|
+
export declare function activeTurnKeyboard(locale: AppLocale, turnId: string): Array<Array<{
|
|
26
|
+
text: string;
|
|
27
|
+
callback_data: string;
|
|
28
|
+
}>>;
|
|
29
|
+
export declare function whereKeyboard(locale: AppLocale, hasBinding: boolean): Array<Array<{
|
|
30
|
+
text: string;
|
|
31
|
+
callback_data: string;
|
|
32
|
+
}>>;
|
|
33
|
+
export declare function renderApprovalMessage(locale: AppLocale, record: PendingApprovalRecord, decision?: ApprovalAction, renderScopeId?: string): string;
|
|
34
|
+
export declare function mapApprovalDecision(record: PendingApprovalRecord, action: ApprovalAction): unknown;
|
|
35
|
+
export declare function parseApprovalPayload(payloadJson: string | null): any;
|
|
36
|
+
export declare function grantedPermissionsFromRequest(requested: any): Record<string, unknown>;
|
|
37
|
+
export declare function parseUserInputQuestions(params: any): PendingUserInputQuestion[];
|
|
38
|
+
export declare function parseServerRequestId(raw: unknown): ServerRequestId | null;
|
|
39
|
+
export declare function stringifyServerRequestId(id: ServerRequestId): string;
|
|
40
|
+
export declare function sameServerRequestId(left: ServerRequestId, right: ServerRequestId): boolean;
|
|
41
|
+
export declare function parseStoredServerRequestId(raw: string): ServerRequestId;
|
|
42
|
+
export declare function serializePendingUserInput(record: PendingUserInputRequest): PendingUserInputStoredRecord;
|
|
43
|
+
export declare function parseStoredPendingUserInput(record: PendingUserInputStoredRecord): PendingUserInputRequest | null;
|
|
44
|
+
export declare function normalizePendingUserInputStatus(raw: string | null | undefined): PendingUserInputStatus;
|
|
45
|
+
export declare function parseStoredUserInputQuestions(rawJson: string): PendingUserInputQuestion[];
|
|
46
|
+
export declare function stringifyPendingUserInputAnswers(answers: Map<string, string>): string;
|
|
47
|
+
export declare function parseStoredUserInputAnswers(rawJson: string): Map<string, string>;
|
|
48
|
+
export declare function normalizeStoredUserInputAnswer(rawAnswer: unknown): string | null;
|
|
49
|
+
export declare function pendingUserInputCurrentQuestionIndex(record: PendingUserInputRequest): number;
|
|
50
|
+
export declare function renderUserInputMessage(locale: AppLocale, record: PendingUserInputRequest): string;
|
|
51
|
+
export declare function userInputKeyboard(record: PendingUserInputRequest): Array<Array<{
|
|
52
|
+
text: string;
|
|
53
|
+
callback_data: string;
|
|
54
|
+
}>>;
|
|
55
|
+
export declare function parseStoredTurnInput(inputJson: string): TurnInput[];
|
|
56
|
+
export declare function parseStagedTelegramAttachments(attachmentsJson: string): StagedTelegramAttachment[];
|
|
57
|
+
export declare function findReusableStandaloneAttachmentBatch(batch: PendingAttachmentBatchRecord | null, now: number): PendingAttachmentBatchRecord | null;
|
|
58
|
+
export declare function mergeAttachmentBatchCaption(existing: string, next: string): string;
|
|
59
|
+
export declare function summarizeStagedAttachmentInput(text: string, attachments: readonly StagedTelegramAttachment[]): string;
|
|
60
|
+
export declare function renderAttachmentBatchMessage(locale: AppLocale, record: PendingAttachmentBatchRecord): string;
|
|
61
|
+
export declare function attachmentBatchKeyboard(locale: AppLocale, batchId: string): Array<Array<{
|
|
62
|
+
text: string;
|
|
63
|
+
callback_data: string;
|
|
64
|
+
}>>;
|
|
65
|
+
export declare function shortId(value: string): string;
|
|
66
|
+
export declare function renderPlanImplementationPrompt(locale: AppLocale, record: PendingPlanImplementation): string;
|
|
67
|
+
export declare function planImplementationKeyboard(locale: AppLocale, localId: string): Array<Array<{
|
|
68
|
+
text: string;
|
|
69
|
+
callback_data: string;
|
|
70
|
+
}>>;
|
|
71
|
+
export declare function threadNewCwdCreateKeyboard(locale: AppLocale): Array<Array<{
|
|
72
|
+
text: string;
|
|
73
|
+
callback_data: string;
|
|
74
|
+
}>>;
|
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
import { t } from '../i18n.js';
|
|
2
|
+
import { BRIDGE_SCOPE_WEIXIN_PREFIX, parseTelegramTargetFromBridgeScope, parseWeixinBridgeScope } from '../core/bridge_scope.js';
|
|
3
|
+
import { ATTACHMENT_BATCH_MERGE_WINDOW_MS, } from './state_types.js';
|
|
4
|
+
import { truncateInline } from './turn_rendering.js';
|
|
5
|
+
import { stringOrNull, clipButtonText } from './shared_helpers.js';
|
|
6
|
+
export function renderMcpElicitationMessage(locale, record, decision) {
|
|
7
|
+
const lines = [
|
|
8
|
+
t(locale, 'mcp_elicitation_requested'),
|
|
9
|
+
t(locale, 'mcp_server_name', { value: record.serverName }),
|
|
10
|
+
t(locale, 'line_thread', { value: record.threadId }),
|
|
11
|
+
];
|
|
12
|
+
if (record.turnId)
|
|
13
|
+
lines.push(t(locale, 'line_turn', { value: record.turnId }));
|
|
14
|
+
lines.push(t(locale, 'mcp_elicitation_message', { value: record.message || t(locale, 'empty') }));
|
|
15
|
+
if (record.url) {
|
|
16
|
+
lines.push(t(locale, 'mcp_elicitation_url', { value: record.url }));
|
|
17
|
+
}
|
|
18
|
+
if (record.mode === 'form') {
|
|
19
|
+
lines.push(t(locale, 'mcp_elicitation_schema', { value: truncateInline(JSON.stringify(record.requestedSchema ?? {}), 1200) }));
|
|
20
|
+
lines.push(record.content === null
|
|
21
|
+
? t(locale, 'mcp_elicitation_reply_json')
|
|
22
|
+
: t(locale, 'mcp_elicitation_json_ready'));
|
|
23
|
+
}
|
|
24
|
+
if (decision) {
|
|
25
|
+
lines.push(t(locale, 'line_decision', { value: decision }));
|
|
26
|
+
}
|
|
27
|
+
if (!decision && parseWeixinBridgeScope(record.chatId)) {
|
|
28
|
+
lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_mcp_title'));
|
|
29
|
+
if (record.mode === 'form') {
|
|
30
|
+
lines.push(t(locale, 'weixin_copy_mcp_json_hint'));
|
|
31
|
+
}
|
|
32
|
+
lines.push(`/mcpel ${record.localId} accept`, `/mcpel ${record.localId} decline`, `/mcpel ${record.localId} cancel`);
|
|
33
|
+
}
|
|
34
|
+
return lines.join('\n');
|
|
35
|
+
}
|
|
36
|
+
export function mcpElicitationKeyboard(locale, record) {
|
|
37
|
+
return [[
|
|
38
|
+
{ text: t(locale, 'button_accept'), callback_data: `mcpel:${record.localId}:accept` },
|
|
39
|
+
{ text: t(locale, 'button_decline'), callback_data: `mcpel:${record.localId}:decline` },
|
|
40
|
+
{ text: t(locale, 'button_cancel'), callback_data: `mcpel:${record.localId}:cancel` },
|
|
41
|
+
]];
|
|
42
|
+
}
|
|
43
|
+
export function formatPermissionRequestSummary(locale, permissions) {
|
|
44
|
+
const lines = [];
|
|
45
|
+
if (permissions?.network?.enabled !== undefined && permissions.network.enabled !== null) {
|
|
46
|
+
lines.push(t(locale, 'permission_network', { value: permissions.network.enabled ? t(locale, 'yes') : t(locale, 'no') }));
|
|
47
|
+
}
|
|
48
|
+
const fsPerms = permissions?.fileSystem;
|
|
49
|
+
if (fsPerms?.read?.length) {
|
|
50
|
+
lines.push(t(locale, 'permission_read_paths', { value: fsPerms.read.slice(0, 5).join(', ') }));
|
|
51
|
+
}
|
|
52
|
+
if (fsPerms?.write?.length) {
|
|
53
|
+
lines.push(t(locale, 'permission_write_paths', { value: fsPerms.write.slice(0, 5).join(', ') }));
|
|
54
|
+
}
|
|
55
|
+
if (Array.isArray(fsPerms?.entries) && fsPerms.entries.length > 0) {
|
|
56
|
+
lines.push(t(locale, 'permission_entries', { value: truncateInline(JSON.stringify(fsPerms.entries.slice(0, 5)), 500) }));
|
|
57
|
+
}
|
|
58
|
+
return lines.join('\n');
|
|
59
|
+
}
|
|
60
|
+
export function resolveScopeMessageTarget(scopeId) {
|
|
61
|
+
if (scopeId.startsWith(BRIDGE_SCOPE_WEIXIN_PREFIX)) {
|
|
62
|
+
const parsed = parseWeixinBridgeScope(scopeId);
|
|
63
|
+
return parsed ? { chatId: parsed.fromUserId, chatType: 'private', topicId: null } : null;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const parsed = parseTelegramTargetFromBridgeScope(scopeId);
|
|
67
|
+
return { chatId: parsed.chatId, chatType: parsed.topicId === null ? 'private' : 'supergroup', topicId: parsed.topicId };
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function restartPreviewRecoveryKey(preview) {
|
|
74
|
+
return `${preview.scopeId}:${preview.turnId}:${preview.messageId}`;
|
|
75
|
+
}
|
|
76
|
+
export function seedObservedTurnCursor(turn) {
|
|
77
|
+
const agentItems = turn.items.filter((item) => {
|
|
78
|
+
const type = item.type.toLowerCase();
|
|
79
|
+
return type === 'agentmessage' || type === 'assistantmessage' || type === 'plan';
|
|
80
|
+
});
|
|
81
|
+
const itemTexts = {};
|
|
82
|
+
for (const item of agentItems) {
|
|
83
|
+
itemTexts[item.itemId] = item.text ?? '';
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
turnId: turn.turnId,
|
|
87
|
+
itemTexts,
|
|
88
|
+
completedItemIds: agentItems.map((item) => item.itemId),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export function observerCursorFromActiveTurn(active) {
|
|
92
|
+
const itemTexts = {};
|
|
93
|
+
const completedItemIds = [];
|
|
94
|
+
for (const segment of active.segments) {
|
|
95
|
+
itemTexts[segment.itemId] = segment.text;
|
|
96
|
+
if (segment.completed) {
|
|
97
|
+
completedItemIds.push(segment.itemId);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
turnId: active.turnId,
|
|
102
|
+
itemTexts,
|
|
103
|
+
completedItemIds,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export function approvalKeyboard(locale, localId) {
|
|
107
|
+
return [[
|
|
108
|
+
{ text: t(locale, 'button_allow'), callback_data: `approval:${localId}:accept` },
|
|
109
|
+
{ text: t(locale, 'button_allow_session'), callback_data: `approval:${localId}:session` },
|
|
110
|
+
{ text: t(locale, 'button_deny'), callback_data: `approval:${localId}:deny` },
|
|
111
|
+
]];
|
|
112
|
+
}
|
|
113
|
+
export function activeTurnKeyboard(locale, turnId) {
|
|
114
|
+
return [[
|
|
115
|
+
{ text: t(locale, 'button_interrupt'), callback_data: `turn:interrupt:${turnId}` },
|
|
116
|
+
]];
|
|
117
|
+
}
|
|
118
|
+
export function whereKeyboard(locale, hasBinding) {
|
|
119
|
+
const firstRow = [
|
|
120
|
+
{ text: t(locale, 'button_permissions'), callback_data: 'nav:permissions' },
|
|
121
|
+
{ text: t(locale, 'button_models'), callback_data: 'nav:models' },
|
|
122
|
+
];
|
|
123
|
+
const secondRow = [{ text: t(locale, 'button_threads'), callback_data: 'nav:threads' }];
|
|
124
|
+
if (!hasBinding) {
|
|
125
|
+
return [firstRow, secondRow];
|
|
126
|
+
}
|
|
127
|
+
return [
|
|
128
|
+
[{ text: t(locale, 'button_reveal'), callback_data: 'nav:reveal' }, { text: t(locale, 'button_permissions'), callback_data: 'nav:permissions' }],
|
|
129
|
+
[{ text: t(locale, 'button_models'), callback_data: 'nav:models' }, { text: t(locale, 'button_threads'), callback_data: 'nav:threads' }],
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
export function renderApprovalMessage(locale, record, decision, renderScopeId = record.chatId) {
|
|
133
|
+
const lines = [
|
|
134
|
+
t(locale, 'approval_requested', {
|
|
135
|
+
kind: record.kind === 'fileChange'
|
|
136
|
+
? t(locale, 'approval_kind_fileChange')
|
|
137
|
+
: record.kind === 'permissions'
|
|
138
|
+
? t(locale, 'approval_kind_permissions')
|
|
139
|
+
: t(locale, 'approval_kind_command'),
|
|
140
|
+
}),
|
|
141
|
+
t(locale, 'line_thread', { value: record.threadId }),
|
|
142
|
+
t(locale, 'line_turn', { value: record.turnId }),
|
|
143
|
+
];
|
|
144
|
+
if (record.command)
|
|
145
|
+
lines.push(t(locale, 'line_command', { value: record.command }));
|
|
146
|
+
if (record.cwd)
|
|
147
|
+
lines.push(t(locale, 'line_cwd', { value: record.cwd }));
|
|
148
|
+
if (record.reason)
|
|
149
|
+
lines.push(t(locale, 'line_reason', { value: record.reason }));
|
|
150
|
+
if (record.kind === 'permissions') {
|
|
151
|
+
const permissions = parseApprovalPayload(record.payloadJson)?.permissions ?? {};
|
|
152
|
+
const summary = formatPermissionRequestSummary(locale, permissions);
|
|
153
|
+
if (summary) {
|
|
154
|
+
lines.push(summary);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (decision) {
|
|
158
|
+
const decisionKey = decision === 'accept'
|
|
159
|
+
? 'approval_decision_accept'
|
|
160
|
+
: decision === 'session'
|
|
161
|
+
? 'approval_decision_session'
|
|
162
|
+
: 'approval_decision_deny';
|
|
163
|
+
lines.push(t(locale, 'line_decision', { value: t(locale, decisionKey) }));
|
|
164
|
+
}
|
|
165
|
+
if (!decision && parseWeixinBridgeScope(renderScopeId)) {
|
|
166
|
+
lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_approval_title'), `/approve ${record.localId} allow`, `/approve ${record.localId} session`, `/approve ${record.localId} deny`);
|
|
167
|
+
}
|
|
168
|
+
return lines.join('\n');
|
|
169
|
+
}
|
|
170
|
+
export function mapApprovalDecision(record, action) {
|
|
171
|
+
if (record.kind === 'permissions') {
|
|
172
|
+
const requested = parseApprovalPayload(record.payloadJson)?.permissions ?? {};
|
|
173
|
+
if (action === 'deny') {
|
|
174
|
+
return { permissions: {}, scope: 'turn' };
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
permissions: grantedPermissionsFromRequest(requested),
|
|
178
|
+
scope: action === 'session' ? 'session' : 'turn',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const decision = action === 'accept'
|
|
182
|
+
? 'accept'
|
|
183
|
+
: action === 'session'
|
|
184
|
+
? 'acceptForSession'
|
|
185
|
+
: 'decline';
|
|
186
|
+
return { decision };
|
|
187
|
+
}
|
|
188
|
+
export function parseApprovalPayload(payloadJson) {
|
|
189
|
+
if (!payloadJson)
|
|
190
|
+
return null;
|
|
191
|
+
try {
|
|
192
|
+
return JSON.parse(payloadJson);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
export function grantedPermissionsFromRequest(requested) {
|
|
199
|
+
const result = {};
|
|
200
|
+
if (requested?.network) {
|
|
201
|
+
result.network = requested.network;
|
|
202
|
+
}
|
|
203
|
+
if (requested?.fileSystem) {
|
|
204
|
+
result.fileSystem = requested.fileSystem;
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
export function parseUserInputQuestions(params) {
|
|
209
|
+
const rawQuestions = Array.isArray(params?.questions)
|
|
210
|
+
? params.questions
|
|
211
|
+
: params?.question
|
|
212
|
+
? [params.question]
|
|
213
|
+
: [];
|
|
214
|
+
const seenIds = new Set();
|
|
215
|
+
return rawQuestions
|
|
216
|
+
.map((raw, index) => {
|
|
217
|
+
const fallbackId = `q${index + 1}`;
|
|
218
|
+
const rawId = stringOrNull(raw?.id) ?? fallbackId;
|
|
219
|
+
const id = seenIds.has(rawId) ? `${rawId}_${index + 1}` : rawId;
|
|
220
|
+
seenIds.add(id);
|
|
221
|
+
const header = stringOrNull(raw?.header);
|
|
222
|
+
const question = stringOrNull(raw?.question) ?? stringOrNull(raw?.prompt) ?? stringOrNull(raw?.text) ?? '';
|
|
223
|
+
const isOther = raw?.isOther === true || raw?.is_other === true;
|
|
224
|
+
const isSecret = raw?.isSecret === true || raw?.is_secret === true;
|
|
225
|
+
const options = Array.isArray(raw?.options)
|
|
226
|
+
? raw.options
|
|
227
|
+
.map((option) => {
|
|
228
|
+
const label = stringOrNull(option?.label) ?? stringOrNull(option?.value) ?? stringOrNull(option);
|
|
229
|
+
if (!label) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
label,
|
|
234
|
+
description: stringOrNull(option?.description),
|
|
235
|
+
};
|
|
236
|
+
})
|
|
237
|
+
.filter((option) => option !== null)
|
|
238
|
+
: [];
|
|
239
|
+
if (!header && !question && options.length === 0) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return { id, header, question, isOther, isSecret, options };
|
|
243
|
+
})
|
|
244
|
+
.filter((question) => question !== null);
|
|
245
|
+
}
|
|
246
|
+
export function parseServerRequestId(raw) {
|
|
247
|
+
if (typeof raw === 'string' || typeof raw === 'number') {
|
|
248
|
+
return raw;
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
export function stringifyServerRequestId(id) {
|
|
253
|
+
return String(id);
|
|
254
|
+
}
|
|
255
|
+
export function sameServerRequestId(left, right) {
|
|
256
|
+
return stringifyServerRequestId(left) === stringifyServerRequestId(right);
|
|
257
|
+
}
|
|
258
|
+
export function parseStoredServerRequestId(raw) {
|
|
259
|
+
if (/^(0|[1-9]\d*)$/.test(raw)) {
|
|
260
|
+
const value = Number(raw);
|
|
261
|
+
if (Number.isSafeInteger(value)) {
|
|
262
|
+
return value;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return raw;
|
|
266
|
+
}
|
|
267
|
+
export function serializePendingUserInput(record) {
|
|
268
|
+
return {
|
|
269
|
+
localId: record.localId,
|
|
270
|
+
serverRequestId: stringifyServerRequestId(record.serverRequestId),
|
|
271
|
+
chatId: record.chatId,
|
|
272
|
+
threadId: record.threadId,
|
|
273
|
+
turnId: record.turnId,
|
|
274
|
+
itemId: record.itemId,
|
|
275
|
+
messageId: record.messageId,
|
|
276
|
+
questionsJson: JSON.stringify(record.questions),
|
|
277
|
+
answersJson: stringifyPendingUserInputAnswers(record.answers),
|
|
278
|
+
currentQuestionIndex: pendingUserInputCurrentQuestionIndex(record),
|
|
279
|
+
awaitingFreeText: false,
|
|
280
|
+
status: record.status,
|
|
281
|
+
createdAt: record.createdAt,
|
|
282
|
+
submittedAt: record.submittedAt,
|
|
283
|
+
resolvedAt: null,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
export function parseStoredPendingUserInput(record) {
|
|
287
|
+
const questions = parseStoredUserInputQuestions(record.questionsJson);
|
|
288
|
+
if (questions.length === 0) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
localId: record.localId,
|
|
293
|
+
serverRequestId: parseStoredServerRequestId(record.serverRequestId),
|
|
294
|
+
chatId: record.chatId,
|
|
295
|
+
threadId: record.threadId,
|
|
296
|
+
turnId: record.turnId,
|
|
297
|
+
itemId: record.itemId,
|
|
298
|
+
questions,
|
|
299
|
+
answers: parseStoredUserInputAnswers(record.answersJson),
|
|
300
|
+
messageId: record.messageId,
|
|
301
|
+
status: normalizePendingUserInputStatus(record.status),
|
|
302
|
+
createdAt: record.createdAt,
|
|
303
|
+
submittedAt: record.submittedAt,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
export function normalizePendingUserInputStatus(raw) {
|
|
307
|
+
if (raw === 'submitted' || raw === 'resolved' || raw === 'interrupted') {
|
|
308
|
+
return raw;
|
|
309
|
+
}
|
|
310
|
+
return 'pending';
|
|
311
|
+
}
|
|
312
|
+
export function parseStoredUserInputQuestions(rawJson) {
|
|
313
|
+
let parsed;
|
|
314
|
+
try {
|
|
315
|
+
parsed = JSON.parse(rawJson);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
if (!Array.isArray(parsed)) {
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
return parsed
|
|
324
|
+
.map((raw, index) => {
|
|
325
|
+
const id = stringOrNull(raw?.id) ?? `q${index + 1}`;
|
|
326
|
+
const question = stringOrNull(raw?.question) ?? '';
|
|
327
|
+
const options = Array.isArray(raw?.options)
|
|
328
|
+
? raw.options
|
|
329
|
+
.map((option) => {
|
|
330
|
+
const label = stringOrNull(option?.label);
|
|
331
|
+
if (!label) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
label,
|
|
336
|
+
description: stringOrNull(option?.description),
|
|
337
|
+
};
|
|
338
|
+
})
|
|
339
|
+
.filter((option) => option !== null)
|
|
340
|
+
: [];
|
|
341
|
+
return {
|
|
342
|
+
id,
|
|
343
|
+
header: stringOrNull(raw?.header),
|
|
344
|
+
question,
|
|
345
|
+
isOther: raw?.isOther === true,
|
|
346
|
+
isSecret: raw?.isSecret === true,
|
|
347
|
+
options,
|
|
348
|
+
};
|
|
349
|
+
})
|
|
350
|
+
.filter((question) => question !== null);
|
|
351
|
+
}
|
|
352
|
+
export function stringifyPendingUserInputAnswers(answers) {
|
|
353
|
+
return JSON.stringify(Object.fromEntries(answers.entries()));
|
|
354
|
+
}
|
|
355
|
+
export function parseStoredUserInputAnswers(rawJson) {
|
|
356
|
+
const answers = new Map();
|
|
357
|
+
let parsed;
|
|
358
|
+
try {
|
|
359
|
+
parsed = JSON.parse(rawJson);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return answers;
|
|
363
|
+
}
|
|
364
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
365
|
+
return answers;
|
|
366
|
+
}
|
|
367
|
+
for (const [id, rawAnswer] of Object.entries(parsed)) {
|
|
368
|
+
const answer = normalizeStoredUserInputAnswer(rawAnswer);
|
|
369
|
+
if (answer !== null) {
|
|
370
|
+
answers.set(id, answer);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return answers;
|
|
374
|
+
}
|
|
375
|
+
export function normalizeStoredUserInputAnswer(rawAnswer) {
|
|
376
|
+
if (typeof rawAnswer === 'string') {
|
|
377
|
+
return rawAnswer;
|
|
378
|
+
}
|
|
379
|
+
if (Array.isArray(rawAnswer)) {
|
|
380
|
+
const first = rawAnswer.find((entry) => typeof entry === 'string');
|
|
381
|
+
return first ?? null;
|
|
382
|
+
}
|
|
383
|
+
if (rawAnswer && typeof rawAnswer === 'object') {
|
|
384
|
+
const nested = rawAnswer.answers;
|
|
385
|
+
return normalizeStoredUserInputAnswer(nested);
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
export function pendingUserInputCurrentQuestionIndex(record) {
|
|
390
|
+
const index = record.questions.findIndex(question => !record.answers.has(question.id));
|
|
391
|
+
return index === -1 ? record.questions.length : index;
|
|
392
|
+
}
|
|
393
|
+
export function renderUserInputMessage(locale, record) {
|
|
394
|
+
const lines = [
|
|
395
|
+
t(locale, 'user_input_requested'),
|
|
396
|
+
t(locale, 'line_thread', { value: record.threadId }),
|
|
397
|
+
];
|
|
398
|
+
if (record.turnId) {
|
|
399
|
+
lines.push(t(locale, 'line_turn', { value: record.turnId }));
|
|
400
|
+
}
|
|
401
|
+
record.questions.forEach((question, index) => {
|
|
402
|
+
lines.push('');
|
|
403
|
+
const title = question.header || question.question || question.id;
|
|
404
|
+
if (question.header && question.question) {
|
|
405
|
+
lines.push(`${index + 1}. **${title}** ${question.question}`);
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
lines.push(`${index + 1}. **${title}**`);
|
|
409
|
+
}
|
|
410
|
+
if (question.isOther) {
|
|
411
|
+
lines.push(` - ${t(locale, 'user_input_other_hint')}`);
|
|
412
|
+
}
|
|
413
|
+
if (question.isSecret) {
|
|
414
|
+
lines.push(` - ${t(locale, 'user_input_secret_warning')}`);
|
|
415
|
+
}
|
|
416
|
+
question.options.forEach((option, optionIndex) => {
|
|
417
|
+
const description = option.description ? ` - ${option.description}` : '';
|
|
418
|
+
lines.push(` ${optionIndex + 1}. ${option.label}${description}`);
|
|
419
|
+
});
|
|
420
|
+
const answer = record.answers.get(question.id);
|
|
421
|
+
if (answer) {
|
|
422
|
+
lines.push(` - ${t(locale, 'user_input_selected', { value: answer })}`);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
lines.push('');
|
|
426
|
+
const statusKey = record.status === 'submitted'
|
|
427
|
+
? 'user_input_submitted_waiting'
|
|
428
|
+
: record.status === 'interrupted'
|
|
429
|
+
? 'user_input_interrupted'
|
|
430
|
+
: record.status === 'resolved'
|
|
431
|
+
? 'user_input_submitted'
|
|
432
|
+
: 'user_input_reply_hint';
|
|
433
|
+
lines.push(t(locale, statusKey));
|
|
434
|
+
if (record.status === 'pending' && parseWeixinBridgeScope(record.chatId)) {
|
|
435
|
+
lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_answer_title'));
|
|
436
|
+
record.questions.forEach((question, questionIndex) => {
|
|
437
|
+
if (record.answers.has(question.id)) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (question.options.length > 0) {
|
|
441
|
+
question.options.forEach((_option, optionIndex) => {
|
|
442
|
+
lines.push(`/answer ${record.localId} ${questionIndex + 1} ${optionIndex + 1}`);
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
lines.push(`/answer ${record.localId} ${questionIndex + 1} <text>`);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return lines.join('\n');
|
|
449
|
+
}
|
|
450
|
+
export function userInputKeyboard(record) {
|
|
451
|
+
const rows = [];
|
|
452
|
+
record.questions.forEach((question, questionIndex) => {
|
|
453
|
+
if (record.answers.has(question.id) || question.options.length === 0) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
rows.push(question.options.map((option, optionIndex) => ({
|
|
457
|
+
text: clipButtonText(record.questions.length > 1 ? `${questionIndex + 1}. ${option.label}` : option.label),
|
|
458
|
+
callback_data: `ui:${record.localId}:${questionIndex}:${optionIndex}`,
|
|
459
|
+
})));
|
|
460
|
+
});
|
|
461
|
+
return rows;
|
|
462
|
+
}
|
|
463
|
+
export function parseStoredTurnInput(inputJson) {
|
|
464
|
+
const value = JSON.parse(inputJson);
|
|
465
|
+
if (!Array.isArray(value)) {
|
|
466
|
+
throw new Error('Queued turn input is not an array');
|
|
467
|
+
}
|
|
468
|
+
return value;
|
|
469
|
+
}
|
|
470
|
+
export function parseStagedTelegramAttachments(attachmentsJson) {
|
|
471
|
+
const value = JSON.parse(attachmentsJson);
|
|
472
|
+
if (!Array.isArray(value)) {
|
|
473
|
+
throw new Error('Attachment batch payload is not an array');
|
|
474
|
+
}
|
|
475
|
+
return value.map((entry) => {
|
|
476
|
+
if (!entry || typeof entry !== 'object') {
|
|
477
|
+
throw new Error('Attachment batch contains an invalid entry');
|
|
478
|
+
}
|
|
479
|
+
const attachment = entry;
|
|
480
|
+
if (typeof attachment.localPath !== 'string' || typeof attachment.relativePath !== 'string') {
|
|
481
|
+
throw new Error('Attachment batch entry is missing a local path');
|
|
482
|
+
}
|
|
483
|
+
return attachment;
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
export function findReusableStandaloneAttachmentBatch(batch, now) {
|
|
487
|
+
if (!batch || batch.mediaGroupId !== null) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
return now - batch.updatedAt <= ATTACHMENT_BATCH_MERGE_WINDOW_MS ? batch : null;
|
|
491
|
+
}
|
|
492
|
+
export function mergeAttachmentBatchCaption(existing, next) {
|
|
493
|
+
const normalizedNext = next.trim();
|
|
494
|
+
if (!normalizedNext) {
|
|
495
|
+
return existing;
|
|
496
|
+
}
|
|
497
|
+
const normalizedExisting = existing.trim();
|
|
498
|
+
if (!normalizedExisting) {
|
|
499
|
+
return normalizedNext;
|
|
500
|
+
}
|
|
501
|
+
return normalizedExisting === normalizedNext ? normalizedExisting : `${normalizedExisting}\n\n${normalizedNext}`;
|
|
502
|
+
}
|
|
503
|
+
export function summarizeStagedAttachmentInput(text, attachments) {
|
|
504
|
+
const lines = [text.trim() || '(no text)'];
|
|
505
|
+
if (attachments.length > 0) {
|
|
506
|
+
lines.push(`[attachments: ${attachments.map((attachment) => `${attachment.kind}:${attachment.fileName}`).join(', ')}]`);
|
|
507
|
+
}
|
|
508
|
+
return lines.join('\n');
|
|
509
|
+
}
|
|
510
|
+
export function renderAttachmentBatchMessage(locale, record) {
|
|
511
|
+
const attachments = parseStagedTelegramAttachments(record.attachmentsJson);
|
|
512
|
+
const lines = [
|
|
513
|
+
t(locale, 'attachment_batch_staged', { count: attachments.length }),
|
|
514
|
+
];
|
|
515
|
+
if (record.caption.trim()) {
|
|
516
|
+
lines.push('', t(locale, 'attachment_batch_caption', { value: truncateInline(record.caption.trim(), 600) }));
|
|
517
|
+
}
|
|
518
|
+
lines.push('');
|
|
519
|
+
for (const [index, attachment] of attachments.entries()) {
|
|
520
|
+
const name = attachment.fileName || attachment.fileUniqueId;
|
|
521
|
+
const size = attachment.fileSize === null ? '' : `, ${attachment.fileSize} bytes`;
|
|
522
|
+
lines.push(`${index + 1}. ${attachment.kind}: ${name}${size}`);
|
|
523
|
+
}
|
|
524
|
+
return lines.join('\n');
|
|
525
|
+
}
|
|
526
|
+
export function attachmentBatchKeyboard(locale, batchId) {
|
|
527
|
+
return [[
|
|
528
|
+
{ text: t(locale, 'attachment_batch_button_analyze'), callback_data: `attach:${batchId}:analyze` },
|
|
529
|
+
{ text: t(locale, 'attachment_batch_button_clear'), callback_data: `attach:${batchId}:clear` },
|
|
530
|
+
]];
|
|
531
|
+
}
|
|
532
|
+
export function shortId(value) {
|
|
533
|
+
return value.slice(0, 8);
|
|
534
|
+
}
|
|
535
|
+
export function renderPlanImplementationPrompt(locale, record) {
|
|
536
|
+
const lines = [
|
|
537
|
+
t(locale, 'plan_impl_title'),
|
|
538
|
+
t(locale, 'line_thread', { value: record.threadId }),
|
|
539
|
+
t(locale, 'line_turn', { value: record.turnId }),
|
|
540
|
+
'',
|
|
541
|
+
t(locale, 'plan_impl_prompt'),
|
|
542
|
+
];
|
|
543
|
+
if (parseWeixinBridgeScope(record.scopeId)) {
|
|
544
|
+
lines.push('', t(locale, 'weixin_copy_paste_divider'), t(locale, 'weixin_copy_plan_title'), `/planimpl ${record.localId} run`, `/planimpl ${record.localId} fresh`, `/planimpl ${record.localId} stay`);
|
|
545
|
+
}
|
|
546
|
+
return lines.join('\n');
|
|
547
|
+
}
|
|
548
|
+
export function planImplementationKeyboard(locale, localId) {
|
|
549
|
+
return [
|
|
550
|
+
[{ text: t(locale, 'plan_impl_button_run'), callback_data: `planimpl:${localId}:run` }],
|
|
551
|
+
[{ text: t(locale, 'plan_impl_button_fresh'), callback_data: `planimpl:${localId}:fresh` }],
|
|
552
|
+
[{ text: t(locale, 'plan_impl_button_stay'), callback_data: `planimpl:${localId}:stay` }],
|
|
553
|
+
];
|
|
554
|
+
}
|
|
555
|
+
export function threadNewCwdCreateKeyboard(locale) {
|
|
556
|
+
return [[
|
|
557
|
+
{ text: t(locale, 'button_create_dir'), callback_data: 'thread:newcwd:create' },
|
|
558
|
+
{ text: t(locale, 'button_cancel'), callback_data: 'thread:newcwd:cancel' },
|
|
559
|
+
]];
|
|
560
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type CodexLocalUsageSnapshot } from '../codex_app/local_usage.js';
|
|
2
|
+
import type { AppConfig } from '../config.js';
|
|
3
|
+
import type { BridgeStore } from '../store/database.js';
|
|
4
|
+
export declare class CodexLocalUsageService {
|
|
5
|
+
private readonly config;
|
|
6
|
+
private readonly store;
|
|
7
|
+
private closed;
|
|
8
|
+
stop(): Promise<void>;
|
|
9
|
+
private localUsageCache;
|
|
10
|
+
private localUsageCacheLoaded;
|
|
11
|
+
private localUsageRefresh;
|
|
12
|
+
constructor(config: AppConfig, store: BridgeStore);
|
|
13
|
+
readCachedCodexLocalUsageStats(): Promise<CodexLocalUsageSnapshot | null>;
|
|
14
|
+
refreshCodexLocalUsageIfNeeded(snapshot?: CodexLocalUsageSnapshot | null): Promise<void>;
|
|
15
|
+
refreshCodexLocalUsageStats(): Promise<void>;
|
|
16
|
+
codexLocalUsageSnapshotPath(): string;
|
|
17
|
+
invalidate(): void;
|
|
18
|
+
private runtimeSnapshotFilename;
|
|
19
|
+
}
|