cortico 0.1.1 → 0.1.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.
Files changed (85) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/src/boot.ts +18 -0
  4. package/src/bot.ts +40 -5
  5. package/src/core/README.md +4 -2
  6. package/src/core/blobs.ts +23 -2
  7. package/src/core/config-schema.ts +1 -1
  8. package/src/core/config.ts +2 -16
  9. package/src/core/core.ts +15 -5
  10. package/src/core/loop.ts +55 -27
  11. package/src/core/session.ts +3 -2
  12. package/src/core/timers.ts +8 -6
  13. package/src/core/types.ts +18 -2
  14. package/src/core/util.ts +15 -0
  15. package/src/deploy.ts +5 -4
  16. package/src/extensions/README.md +6 -4
  17. package/src/extensions/dry-mount.ts +23 -2
  18. package/src/extensions/manifest.ts +21 -11
  19. package/src/extensions.ts +147 -23
  20. package/src/launcher.ts +23 -9
  21. package/src/protocol/open-responses/context-log.ts +37 -9
  22. package/src/providers/README.md +32 -7
  23. package/src/providers/base.ts +2 -0
  24. package/src/providers/console/config.ts +21 -0
  25. package/src/providers/console/hub.ts +270 -0
  26. package/src/providers/console/settings.ts +11 -19
  27. package/src/providers/console/types.ts +5 -0
  28. package/src/providers/hub-api.ts +3 -0
  29. package/src/providers/llamacpp/config.ts +38 -0
  30. package/src/providers/llamacpp/console/runtime-panel.ts +36 -73
  31. package/src/providers/llamacpp/console/server.ts +23 -2
  32. package/src/providers/llamacpp/index.ts +5 -1
  33. package/src/providers/llamacpp/native.ts +10 -5
  34. package/src/providers/llamacpp/options.ts +2 -0
  35. package/src/providers/name.ts +8 -0
  36. package/src/providers/openai-responses-compat/config.ts +33 -0
  37. package/src/providers/openai-responses-compat/console/client.ts +5 -0
  38. package/src/providers/openai-responses-compat/console/reasoning-panel.ts +84 -0
  39. package/src/providers/openai-responses-compat/console/server.ts +127 -0
  40. package/src/providers/openai-responses-compat/index.ts +40 -12
  41. package/src/providers/openai-responses-compat/native.ts +9 -4
  42. package/src/providers/openai-responses-compat/strings.ts +62 -1
  43. package/src/providers/registry.ts +5 -0
  44. package/src/providers/transport/responses-input.ts +59 -10
  45. package/src/web/README.md +12 -3
  46. package/src/web/auth.ts +80 -0
  47. package/src/web/client/console-pages/builtins/llm-settings/panel.ts +42 -35
  48. package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +22 -8
  49. package/src/web/client/console-pages/builtins/llm-settings/strings.ts +2 -4
  50. package/src/web/client/console-pages/host.ts +22 -5
  51. package/src/web/client/core/api.ts +5 -1
  52. package/src/web/client/core/router.ts +15 -0
  53. package/src/web/client/features/extensions/index.ts +276 -41
  54. package/src/web/client/features/extensions/strings.ts +84 -10
  55. package/src/web/client/features/feature.ts +1 -1
  56. package/src/web/client/features/live/diagnostics.ts +32 -0
  57. package/src/web/client/features/live/index.ts +27 -11
  58. package/src/web/client/features/live/protocol.ts +1 -0
  59. package/src/web/client/features/live/strings.ts +9 -3
  60. package/src/web/client/features/providers/detail.ts +262 -0
  61. package/src/web/client/features/providers/drafts.ts +23 -0
  62. package/src/web/client/features/providers/index.ts +173 -87
  63. package/src/web/client/features/providers/strings.ts +44 -17
  64. package/src/web/client/features/providers/types.ts +15 -0
  65. package/src/web/client/features/settings/general.ts +12 -0
  66. package/src/web/client/features/settings/strings.ts +6 -0
  67. package/src/web/client/main.ts +4 -0
  68. package/src/web/client/shell/index.ts +1 -1
  69. package/src/web/client/ui/icons.ts +9 -1
  70. package/src/web/client/ui/prompt-input.tsx +17 -5
  71. package/src/web/client/ui/strings.ts +0 -2
  72. package/src/web/diagnostics.ts +133 -0
  73. package/src/web/public/login.html +67 -0
  74. package/src/web/public/styles.css +122 -18
  75. package/src/web/server.ts +227 -21
  76. package/src/web/shared/client-panel.ts +3 -0
  77. package/src/web/shared/console-protocol.ts +3 -1
  78. package/src/worlds/bilibili/README.md +1 -1
  79. package/src/worlds/bilibili/overlay/server.ts +4 -2
  80. package/src/worlds/minecraft/ADAPT.md +66 -0
  81. package/src/worlds/minecraft/README.md +8 -0
  82. package/src/worlds/minecraft/mineflayer-fixes.ts +55 -1
  83. package/src/worlds/qq/normalize.ts +14 -0
  84. package/src/worlds/qq/world.ts +53 -7
  85. package/src/worlds/terminal/world.ts +3 -4
@@ -1,23 +1,50 @@
1
1
  import { pick } from '../../core/language.ts';
2
-
3
2
  const zh = {
4
- pageTitle: '模型提供商',
5
- modulesAria: '供应模块',
6
- needHost: "模型提供商设置不可用。",
7
- loading: '读取供应模块…',
8
- none: '没有已注册的供应模块。',
9
- navLabel: '模型提供商',
10
- navGroup: 'Core',
3
+ pageTitle: '模型供应商', navLabel: '模型供应商', navGroup: 'Core', create: '+ 新建供应商实例', newName: '新建实例',
4
+ createHint: '实例 = 填一组供应商配置保存成卡片',
5
+ empty: '还没有模型供应商', emptyHint: '添加一条模型连接后,Cortico 才能进行模型推理。',
6
+ configure: '⚙ 配置', activate: '⇄ 设为当前', active: '当前模型', missing: '当前模型供应商不存在,配置中引用:',
7
+ eraseConfirm: '确认删除', probe: '测试可用性', probeClose: '收起',
8
+ readiness: { ready: '配置完成', 'needs-setup': '需要完善配置', 'runtime-unavailable': '运行环境未就绪', invalid: '配置错误', 'module-missing': '模块不可用', draft: '草稿' } as Record<string, string>,
9
+ basic: '基本信息', connection: '连接', modelSection: '模型与生成', moduleSection: '模块设置', pricing: '成本与计价', advanced: '高级协议',
10
+ name: '供应商名称', module: '供应商类型', url: 'API 地址', key: 'API Key', model: '模型',
11
+ moduleNote: (description: string, id: string) => `${description} 模块标识:${id}`,
12
+ nameHint: '英文字母、数字、- 或 _,以字母或数字开头,不允许空格或系统保留名。',
13
+ fixedModule: '供应商类型保存后不可更改。使用其他类型需新建供应商。',
14
+ test: '测试连接', testOk: '测试成功', testFailed: '测试失败', savedFirst: '请先保存配置,再执行此操作。',
15
+ keySet: '已配置;留空保留现有密钥', keyEmpty: '输入 API Key', fetchModels: '获取模型列表',
16
+ reasoning: '推理强度', thinking: '开启推理', temperature: '温度', maxTokens: '最大输出 token', context: '上下文上限', tier: '服务档位', images: '接受图片',
17
+ advancedHint: '仅在使用自定义 API 网关或兼容服务时调整。', secret: '密钥环境变量名',
18
+ extraHeaders: '附加请求头(JSON object)', extraBody: '附加请求体(JSON object)', priceRules: '完整计价规则(JSON array)',
19
+ shared: '共享配置:此供应商可被多个 Bot 使用。修改会改变这份共享配置;若需要不同参数,建议新建一个供应商。',
20
+ remove: '删除供应商', duplicate: '复制供应商', cancel: '取消', draft: '保存草稿', save: '保存', saved: '已保存', drafted: '草稿已保存到此浏览器。API Key 不保存在浏览器草稿中。',
21
+ chooseModule: '请选择供应商类型。', invalidNumber: '请输入有效范围内的数值。',
22
+ required: '此项必填。', jsonObject: '请输入合法 JSON 对象。', jsonArray: '请输入合法 JSON 数组。',
23
+ unsaved: '你有未保存的更改。', stay: '留在这里', discard: '放弃更改并切换',
24
+ deleteConfirm: '删除供应商及其目录中的全部文件?', referenced: '此供应商正在被以下 Bot 使用,不能删除:', reload: '重新加载',
11
25
  };
12
-
13
26
  const en: typeof zh = {
14
- pageTitle: 'LLM Provider',
15
- modulesAria: 'Provider modules',
16
- needHost: "LLM provider settings unavailable.",
17
- loading: 'Loading provider modules…',
18
- none: 'No provider modules registered.',
19
- navLabel: 'LLM Provider',
20
- navGroup: 'Core',
27
+ pageTitle: 'Model connections', navLabel: 'Model connections', navGroup: 'Core', create: '+ New connection instance', newName: 'New instance',
28
+ createHint: 'An instance is one set of provider settings saved as a card.',
29
+ empty: 'No model connections yet', emptyHint: 'Add a model connection to enable inference.',
30
+ configure: ' Configure', activate: '⇄ Set current', active: 'Current model', missing: 'Current model connection is missing: ',
31
+ eraseConfirm: 'Confirm delete', probe: 'Test endpoint', probeClose: 'Hide',
32
+ readiness: { ready: 'Configured', 'needs-setup': 'Needs setup', 'runtime-unavailable': 'Runtime unavailable', invalid: 'Invalid configuration', 'module-missing': 'Module unavailable', draft: 'Draft' },
33
+ basic: 'Basic information', connection: 'Connection', modelSection: 'Model and generation', moduleSection: 'Module settings', pricing: 'Pricing', advanced: 'Advanced protocol',
34
+ name: 'Connection name', module: 'Connection type', url: 'API URL', key: 'API Key', model: 'Model',
35
+ moduleNote: (description: string, id: string) => `${description} Module ID: ${id}`,
36
+ nameHint: 'English letters, digits, - or _; start with a letter or digit. No spaces or reserved system names.',
37
+ fixedModule: 'The connection type cannot change after saving. Create a connection to use another type.',
38
+ test: 'Test connection', testOk: 'Test successful', testFailed: 'Test failed', savedFirst: 'Save the configuration before this operation.',
39
+ keySet: 'Configured; leave blank to keep the current key', keyEmpty: 'Enter API Key', fetchModels: 'Fetch model list',
40
+ reasoning: 'Reasoning effort', thinking: 'Enable reasoning', temperature: 'Temperature', maxTokens: 'Max output tokens', context: 'Context limit', tier: 'Service tier', images: 'Accept images',
41
+ advancedHint: 'Adjust only for custom API gateways or compatible services.', secret: 'Credential environment variable',
42
+ extraHeaders: 'Extra headers (JSON object)', extraBody: 'Extra request body (JSON object)', priceRules: 'Full pricing rules (JSON array)',
43
+ shared: 'Shared configuration: multiple bots can use this connection. Changes update this shared configuration; create another connection if you need different settings.',
44
+ remove: 'Delete connection', duplicate: 'Duplicate connection', cancel: 'Cancel', draft: 'Save draft', save: 'Save', saved: 'Saved', drafted: 'Draft saved in this browser. API Keys are not stored in browser drafts.',
45
+ chooseModule: 'Choose a connection type.', invalidNumber: 'Enter a number within the allowed range.',
46
+ required: 'Required.', jsonObject: 'Enter a valid JSON object.', jsonArray: 'Enter a valid JSON array.',
47
+ unsaved: 'You have unsaved changes.', stay: 'Stay here', discard: 'Discard and switch',
48
+ deleteConfirm: 'Delete this connection and all files in its directory?', referenced: 'This connection is used by these bots and cannot be deleted: ', reload: 'Reload',
21
49
  };
22
-
23
50
  export const S = pick({ zh, en });
@@ -0,0 +1,15 @@
1
+ import type { ProviderHubApi } from '../../../../providers/hub-api.ts';
2
+ export type HubState = ReturnType<ProviderHubApi['list']>;
3
+ export type Connection = HubState['providers'][number];
4
+ export type Detail = ReturnType<ProviderHubApi['detail']>;
5
+ export type Module = ReturnType<ProviderHubApi['moduleList']>[number];
6
+ export interface Editing {
7
+ original: string | null;
8
+ copyFrom?: { name: string; revision: string };
9
+ name: string;
10
+ entry: Detail['entry'];
11
+ revision?: string;
12
+ secretValue: string;
13
+ raw: Record<string, string>;
14
+ }
15
+ export const connectionPath = (name: string) => `/api/providers/${encodeURIComponent(name)}`;
@@ -1,4 +1,5 @@
1
1
  import { LANGUAGE, saveLanguage, type Language } from '../../core/language.ts';
2
+ import { post } from '../../core/api.ts';
2
3
  import type { FeatureContext } from '../feature.ts';
3
4
  import { S } from './strings.ts';
4
5
 
@@ -21,6 +22,17 @@ export function mountGeneral(ctx: FeatureContext): void {
21
22
  group.append(button);
22
23
  }
23
24
 
25
+ if (ctx.capabilities.auth) {
26
+ const access = ui.sheet({ title: S.access, en: 'access', desc: S.accessDesc });
27
+ const signOut = ui.h('button', 'btn', S.signOut);
28
+ signOut.type = 'button';
29
+ signOut.addEventListener('click', () => {
30
+ void post('/api/auth/logout', {}, { signal }).then(() => win.location.reload()).catch(ctx.onError);
31
+ }, { signal });
32
+ access.body.appendChild(signOut);
33
+ root.append(access.el);
34
+ }
35
+
24
36
  async function changeLanguage(language: Language): Promise<void> {
25
37
  if (!await ui.confirm({ title: S.reloadTitle, body: S.reloadBody }) || signal.aborted) return;
26
38
  saveLanguage(language, win.localStorage);
@@ -7,6 +7,9 @@ const zh = {
7
7
  languageDesc: '仅保存在当前浏览器,刷新页面后生效。',
8
8
  reloadTitle: '切换界面语言?',
9
9
  reloadBody: '页面将刷新,未保存的编辑会丢失。Bot 将继续运行。',
10
+ access: '访问',
11
+ accessDesc: '这个控制台设置了访问密码;登录态保存在当前浏览器的 Cookie 里。',
12
+ signOut: '退出登录',
10
13
  appearance: '外观',
11
14
  appearanceDesc: '控制台主题、明暗模式与自定义配色。',
12
15
 
@@ -22,6 +25,9 @@ const en: typeof zh = {
22
25
  languageDesc: 'Saved in this browser; takes effect after reloading.',
23
26
  reloadTitle: 'Change interface language?',
24
27
  reloadBody: 'The page will reload and unsaved edits will be lost. The bot will keep running.',
28
+ access: 'Access',
29
+ accessDesc: 'This console is protected by an access password; the sign-in is kept in a cookie in this browser.',
30
+ signOut: 'Sign out',
25
31
  appearance: 'Appearance',
26
32
  appearanceDesc: 'Console theme, light/dark mode and custom palettes.',
27
33
 
@@ -215,6 +215,10 @@ export function boot(doc: Document = document): { dispose(): void } {
215
215
  }
216
216
  const head = route.segments[0];
217
217
 
218
+ if (head === PROVIDER_ROUTE && route.segments[1]?.startsWith('llm:')) {
219
+ router.replace(['providers']);
220
+ return;
221
+ }
218
222
  if (head === PROVIDER_ROUTE) {
219
223
  unmountFeature();
220
224
  const pageId = route.segments[1];
@@ -360,7 +360,7 @@ export function createShell(deps: ShellDeps): ConsoleShell {
360
360
 
361
361
  // 只列已装配的:左栏是"能去的地方",不是全量清单。未装配的仍由各自的
362
362
  // 总览页负责露面(那里才有"为什么没装上"的位置)。供应模块(kind `llm`)不在
363
- // 这里逐个列出:它们的入口是框架的「语言模型」页,模块清单是那一页里的次级菜单。
363
+ // 这里逐个列出:它们的入口是框架的「模型提供商」页,模块清单是那一页里的次级菜单。
364
364
  const listed = consolePages.filter((p) => p.availability === 'active');
365
365
  // Persona 页在前,它的 Memory 页跟在后面,再是归这一组的框架页(系统提示词),同一组。
366
366
  const personas = [
@@ -18,7 +18,8 @@ export type ConsoleIconName =
18
18
  | 'eye'
19
19
  | 'eye-off'
20
20
  | 'pencil'
21
- | 'refresh';
21
+ | 'refresh'
22
+ | 'trash';
22
23
 
23
24
  type Shape = readonly [tag: 'path' | 'circle' | 'rect' | 'line', attrs: Readonly<Record<string, string>>];
24
25
 
@@ -102,6 +103,13 @@ const SHAPES: Readonly<Record<ConsoleIconName, readonly Shape[]>> = {
102
103
  ['path', { d: 'M21 12a9 9 0 1 1-2.6-6.4' }],
103
104
  ['path', { d: 'M21 3v6h-6' }],
104
105
  ],
106
+ trash: [
107
+ ['path', { d: 'M4 7h16' }],
108
+ ['path', { d: 'M9.5 7V5h5v2' }],
109
+ ['path', { d: 'M6.5 7v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V7' }],
110
+ ['path', { d: 'M10 11v6' }],
111
+ ['path', { d: 'M14 11v6' }],
112
+ ],
105
113
  };
106
114
 
107
115
  /**
@@ -44,7 +44,7 @@ const TEXTAREA_CLASS =
44
44
 
45
45
  /** 两枚圆形图标钮共用的底:尺寸、焦点环、禁用态。 */
46
46
  const ICON_BUTTON =
47
- 'inline-flex size-8 shrink-0 items-center justify-center rounded-full p-0 outline-none transition-[background-color,color,opacity,transform] duration-150 disabled:pointer-events-none disabled:opacity-40 focus-visible:ring-2 focus-visible:ring-ring/40 [&_svg]:pointer-events-none [&_svg]:size-4';
47
+ 'inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full p-0 outline-none transition-[background-color,color,opacity,transform] duration-150 disabled:pointer-events-none disabled:opacity-40 focus-visible:ring-2 focus-visible:ring-ring/40 [&_svg]:pointer-events-none [&_svg]:size-4';
48
48
 
49
49
  /** 发送钮:实底,是这一栏唯一的主动作。 */
50
50
  const SEND_CLASS = `${ICON_BUTTON} self-end bg-foreground text-background hover:bg-foreground/90 active:translate-y-px focus-visible:ring-offset-2 focus-visible:ring-offset-background`;
@@ -124,6 +124,7 @@ const PromptComposer = React.forwardRef<ComposerHandle, { opts: ConsolePromptInp
124
124
  const [placeholder, setPlaceholder] = React.useState<string | null>(null);
125
125
  const textarea = React.useRef<HTMLTextAreaElement>(null);
126
126
  const tools = React.useRef<HTMLSpanElement>(null);
127
+ const leading = React.useRef<HTMLSpanElement>(null);
127
128
  const picker = React.useRef<HTMLInputElement>(null);
128
129
  const nextId = React.useRef(1);
129
130
  const imagesOpts = opts.images;
@@ -137,6 +138,14 @@ const PromptComposer = React.forwardRef<ComposerHandle, { opts: ConsolePromptInp
137
138
  return () => node.remove();
138
139
  }, [opts.tools]);
139
140
 
141
+ React.useLayoutEffect(() => {
142
+ const node = opts.leading;
143
+ const host = leading.current;
144
+ if (!node || !host) return;
145
+ host.appendChild(node);
146
+ return () => node.remove();
147
+ }, [opts.leading]);
148
+
140
149
  React.useImperativeHandle(ref, () => ({
141
150
  focus: () => textarea.current?.focus(),
142
151
  setDisabled,
@@ -237,10 +246,13 @@ const PromptComposer = React.forwardRef<ComposerHandle, { opts: ConsolePromptInp
237
246
  />
238
247
  </div>
239
248
  <div className="flex min-h-9 items-center justify-between gap-2 px-1">
240
- <span className="truncate px-2 text-xs text-muted-foreground">
241
- {note ? <span className="text-danger">{note}</span>
242
- : pending > 0 ? S.processingImages(pending)
243
- : opts.hint ?? S.enterToSend}
249
+ <span className="flex min-w-0 items-center gap-1">
250
+ <span ref={leading} className="flex items-center" />
251
+ <span className="truncate px-1 text-xs text-muted-foreground">
252
+ {note ? <span className="text-danger">{note}</span>
253
+ : pending > 0 ? S.processingImages(pending)
254
+ : opts.hint}
255
+ </span>
244
256
  </span>
245
257
  <div className="flex items-center gap-1">
246
258
  {imagesOpts && (
@@ -27,7 +27,6 @@ const zh = {
27
27
  messageInput: '消息输入',
28
28
  typeMessage: '输入消息…',
29
29
  processingImages: (count: number) => `处理 ${count} 张图片…`,
30
- enterToSend: 'Enter 发送 · Shift+Enter 换行',
31
30
  sendMessage: '发送消息',
32
31
  addImage: '添加图片',
33
32
  addImageHint: '添加图片(也可粘贴或拖入)',
@@ -61,7 +60,6 @@ const en: typeof zh = {
61
60
  messageInput: 'Message input',
62
61
  typeMessage: 'Type a message…',
63
62
  processingImages: (count: number) => `Processing ${count} image(s)…`,
64
- enterToSend: 'Enter to send · Shift+Enter for newline',
65
63
  sendMessage: 'Send message',
66
64
  addImage: 'Add image',
67
65
  addImageHint: 'Add image (paste or drop also works)',
@@ -0,0 +1,133 @@
1
+ /**
2
+ * 诊断包:一次导出把排查一场跑要看的记录收进一个 JSON —— 运行指纹、状态快照、常驻 session
3
+ * 的上下文、事件、运行日志、工具调用、交接边界、用量与脱敏配置。
4
+ *
5
+ * 每段各有条数上限,取到上限的段名列在 `truncated` 里;配置里键名含 secret / token / key /
6
+ * password 的值在写出前抹掉。缺席的接缝(未挂载调试通道、未挂载用量)那一段为空。
7
+ */
8
+
9
+ import { dirname, join } from 'node:path';
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import type { ContextRecord } from '../protocol/open-responses/context.ts';
12
+ import type { EventEnvelope, LogRecord } from '../core/types.ts';
13
+ import type { SessionStats } from '../core/sessions.ts';
14
+ import { redactSecrets } from '../core/util.ts';
15
+ import { logPredicate, readRunsIndex, readTailRecordsWhere, type RunIndexRow } from './files.ts';
16
+
17
+ /**
18
+ * 每段取最近多少条,与 `/api/log` 的缺省条数一致。事件段非它不可:`store.range` 不给 limit
19
+ * 会把事件库整份读出来。取到这个数的段名进 `truncated`,拿到包的人据此知道还有更早的记录。
20
+ */
21
+ export const DIAGNOSTICS_TAIL = 200;
22
+
23
+ export interface DiagnosticsToolSchema {
24
+ name: string;
25
+ description: string;
26
+ parameters: Record<string, unknown>;
27
+ }
28
+
29
+ /** 活数据由 WebApp 现取交进来;落盘记录由本模块按 `dataDir` 与 `runId` 自己读。 */
30
+ export interface DiagnosticsSources {
31
+ dataDir: string;
32
+ runId: string | null;
33
+ exportedAt: Date;
34
+ status: Record<string, unknown>;
35
+ /** 常驻 session 的上下文与合成开头;没挂调试通道时为 null。 */
36
+ session: { messages: readonly ContextRecord[]; head: readonly ContextRecord[] } | null;
37
+ sessions: readonly SessionStats[];
38
+ toolSchemas: readonly DiagnosticsToolSchema[];
39
+ events: readonly EventEnvelope[];
40
+ latestCursor: number | null;
41
+ worlds: readonly unknown[];
42
+ /** `/api/usage` 那份聚合;没挂用量时为 null。 */
43
+ usage: unknown;
44
+ }
45
+
46
+ export interface DiagnosticsBundle {
47
+ kind: 'cortico-diagnostics';
48
+ version: 1;
49
+ exportedAt: string;
50
+ run: { id: string | null; index: RunIndexRow | null; manifest: unknown };
51
+ status: Record<string, unknown>;
52
+ worlds: readonly unknown[];
53
+ session: { messages: readonly ContextRecord[]; head: readonly ContextRecord[] } | null;
54
+ sessions: readonly SessionStats[];
55
+ toolSchemas: readonly DiagnosticsToolSchema[];
56
+ events: { latestCursor: number | null; items: readonly EventEnvelope[] };
57
+ /** `warn` 是 warn 及以上,`tail` 不论级别取最近几条。 */
58
+ log: { warn: LogRecord[]; tail: LogRecord[] };
59
+ toolcalls: unknown[];
60
+ /** transcript 里的 handoff / clear / prefix-reload 记号。 */
61
+ transcriptBoundaries: unknown[];
62
+ usage: { aggregate: unknown; rows: unknown[] };
63
+ config: unknown;
64
+ truncated: string[];
65
+ }
66
+
67
+ function readJson(file: string): unknown {
68
+ return existsSync(file) ? (JSON.parse(readFileSync(file, 'utf8')) as unknown) : null;
69
+ }
70
+
71
+ export function buildDiagnostics(sources: DiagnosticsSources): DiagnosticsBundle {
72
+ const { dataDir, runId } = sources;
73
+ const runDir = runId ? join(dataDir, 'runs', runId) : null;
74
+ const warn = runDir
75
+ ? readTailRecordsWhere<LogRecord>(join(runDir, 'log.jsonl'), DIAGNOSTICS_TAIL, logPredicate({ level: 'warn' }))
76
+ : [];
77
+ const tail = runDir
78
+ ? readTailRecordsWhere<LogRecord>(join(runDir, 'log.jsonl'), DIAGNOSTICS_TAIL, () => true)
79
+ : [];
80
+ const toolcalls = runDir
81
+ ? readTailRecordsWhere<unknown>(join(runDir, 'toolcalls.jsonl'), DIAGNOSTICS_TAIL, () => true)
82
+ : [];
83
+ const boundaries = runDir
84
+ ? readTailRecordsWhere<{ kind?: string }>(
85
+ join(runDir, 'transcript.jsonl'),
86
+ DIAGNOSTICS_TAIL,
87
+ (r) => r?.kind === 'boundary',
88
+ )
89
+ : [];
90
+ const usageRows = runId
91
+ ? readTailRecordsWhere<{ run?: string }>(
92
+ join(dataDir, 'usage.jsonl'),
93
+ DIAGNOSTICS_TAIL,
94
+ (r) => r?.run === runId,
95
+ )
96
+ : [];
97
+ const events = sources.events.slice(-DIAGNOSTICS_TAIL);
98
+
99
+ const truncated: string[] = [];
100
+ for (const [name, rows] of [
101
+ ['events', events],
102
+ ['log.warn', warn],
103
+ ['log.tail', tail],
104
+ ['toolcalls', toolcalls],
105
+ ['transcriptBoundaries', boundaries],
106
+ ['usage.rows', usageRows],
107
+ ] as const) {
108
+ if (rows.length >= DIAGNOSTICS_TAIL) truncated.push(name);
109
+ }
110
+
111
+ const index = runId
112
+ ? readRunsIndex(join(dataDir, 'runs', 'index.jsonl')).find((row) => row.run === runId) ?? null
113
+ : null;
114
+
115
+ return {
116
+ kind: 'cortico-diagnostics',
117
+ version: 1,
118
+ exportedAt: sources.exportedAt.toISOString(),
119
+ run: { id: runId, index, manifest: runDir ? readJson(join(runDir, 'run.json')) : null },
120
+ status: sources.status,
121
+ worlds: sources.worlds,
122
+ session: sources.session,
123
+ sessions: sources.sessions,
124
+ toolSchemas: sources.toolSchemas,
125
+ events: { latestCursor: sources.latestCursor, items: events },
126
+ log: { warn, tail },
127
+ toolcalls,
128
+ transcriptBoundaries: boundaries,
129
+ usage: { aggregate: sources.usage, rows: usageRows },
130
+ config: redactSecrets(readJson(join(dirname(dataDir), 'config.json'))),
131
+ truncated,
132
+ };
133
+ }
@@ -0,0 +1,67 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>登录 · Cortico</title>
7
+ <style>
8
+ :root { color-scheme: light dark; --bg: #f4f5f4; --card: #fff; --ink: #1d2321; --dim: #66706c; --line: #d5dad7; --accent: #2f7d62; --bad: #b3261e; }
9
+ @media (prefers-color-scheme: dark) {
10
+ :root { --bg: #141716; --card: #1d2120; --ink: #e6ebe8; --dim: #98a39e; --line: #333a37; --accent: #5fc29f; --bad: #f2867f; }
11
+ }
12
+ * { box-sizing: border-box; }
13
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: var(--bg); color: var(--ink); font: 15px/1.5 system-ui, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; }
14
+ form { width: min(360px, calc(100vw - 32px)); padding: 28px; background: var(--card); border: 1px solid var(--line); border-radius: 12px; }
15
+ h1 { margin: 0 0 4px; font-size: 18px; font-weight: 600; }
16
+ p { margin: 0 0 20px; color: var(--dim); font-size: 13px; }
17
+ label { display: block; margin-bottom: 6px; font-size: 13px; color: var(--dim); }
18
+ input { width: 100%; padding: 9px 11px; font: inherit; color: inherit; background: var(--bg); border: 1px solid var(--line); border-radius: 8px; }
19
+ input:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
20
+ button { width: 100%; margin-top: 16px; padding: 9px 11px; font: inherit; font-weight: 600; color: var(--card); background: var(--accent); border: 0; border-radius: 8px; cursor: pointer; }
21
+ button:disabled { opacity: .6; cursor: default; }
22
+ #error { min-height: 20px; margin: 12px 0 0; color: var(--bad); font-size: 13px; }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <form id="login" autocomplete="on">
27
+ <h1 data-t="title"></h1>
28
+ <p data-t="hint"></p>
29
+ <label for="password" data-t="label"></label>
30
+ <input id="password" name="password" type="password" autocomplete="current-password" required autofocus>
31
+ <button type="submit" data-t="submit"></button>
32
+ <div id="error" role="alert"></div>
33
+ </form>
34
+ <script>
35
+ (() => {
36
+ const en = document.documentElement.lang === 'en';
37
+ const T = en
38
+ ? { title: 'Cortico console', hint: 'This console is protected by an access password.', label: 'Password', submit: 'Sign in', wrong: 'Wrong password.', failed: 'Sign-in failed: ' }
39
+ : { title: 'Cortico 控制台', hint: '这个控制台设置了访问密码。', label: '密码', submit: '登录', wrong: '密码不对。', failed: '登录失败:' };
40
+ document.title = (en ? 'Sign in' : '登录') + ' · Cortico';
41
+ for (const el of document.querySelectorAll('[data-t]')) el.textContent = T[el.dataset.t];
42
+ const form = document.getElementById('login');
43
+ const input = document.getElementById('password');
44
+ const button = form.querySelector('button');
45
+ const error = document.getElementById('error');
46
+ form.addEventListener('submit', async (event) => {
47
+ event.preventDefault();
48
+ button.disabled = true;
49
+ error.textContent = '';
50
+ try {
51
+ const res = await fetch('/api/auth/login', {
52
+ method: 'POST',
53
+ headers: { 'Content-Type': 'application/json' },
54
+ body: JSON.stringify({ password: input.value }),
55
+ });
56
+ if (res.ok) { location.reload(); return; }
57
+ error.textContent = res.status === 401 ? T.wrong : T.failed + 'HTTP ' + res.status;
58
+ } catch (err) {
59
+ error.textContent = T.failed + String(err);
60
+ }
61
+ button.disabled = false;
62
+ input.select();
63
+ });
64
+ })();
65
+ </script>
66
+ </body>
67
+ </html>