cotutor 0.4.4 → 0.4.5

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 (55) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/INSTALL.md +159 -0
  3. package/README.md +35 -19
  4. package/dist/app/cli/activities.d.ts +1 -1
  5. package/dist/app/cli/activities.js +2 -2
  6. package/dist/app/cli/cert.js +2 -1
  7. package/dist/app/cli/chalk/main.d.ts +0 -1
  8. package/dist/app/cli/chalk/main.js +8 -12
  9. package/dist/app/cli/commands.d.ts +50 -0
  10. package/dist/app/cli/commands.js +353 -0
  11. package/dist/app/cli/doctor.js +32 -17
  12. package/dist/app/cli/guide.d.ts +6 -0
  13. package/dist/app/cli/guide.js +17 -0
  14. package/dist/app/cli/init.d.ts +2 -0
  15. package/dist/app/cli/init.js +60 -6
  16. package/dist/app/cli/main.js +109 -46
  17. package/dist/app/cli/migrate.js +1 -1
  18. package/dist/app/cli/port.js +2 -10
  19. package/dist/app/cli/sandbox.js +2 -7
  20. package/dist/app/cli/service.d.ts +4 -1
  21. package/dist/app/cli/service.js +5 -2
  22. package/dist/app/cli/shipped.js +1 -1
  23. package/dist/app/cli/skeleton.d.ts +2 -0
  24. package/dist/app/cli/skeleton.js +3 -2
  25. package/dist/app/cli/skills.js +2 -2
  26. package/dist/app/cli/themes.js +2 -2
  27. package/dist/app/cli/tray.d.ts +6 -1
  28. package/dist/app/cli/tray.js +8 -5
  29. package/dist/app/cli/tutors.d.ts +2 -2
  30. package/dist/app/cli/tutors.js +4 -4
  31. package/dist/app/cli/update-cmd.js +4 -4
  32. package/dist/app/cli/usage.d.ts +26 -2
  33. package/dist/app/cli/usage.js +167 -38
  34. package/dist/app/http/app.js +1 -1
  35. package/dist/app/skills/analyze-doc.js +4 -8
  36. package/dist/app/skills/home-doc.js +3 -7
  37. package/dist/app/skills/tune-doc.js +4 -8
  38. package/dist/app/workspace.d.ts +16 -3
  39. package/dist/app/workspace.js +31 -9
  40. package/dist/runtime/home/home.js +1 -1
  41. package/dist/runtime/runner.js +2 -2
  42. package/dist/shared/home.d.ts +2 -0
  43. package/dist/shared/home.js +9 -0
  44. package/dist/ui/dev/dev-page.js +5 -5
  45. package/dist/ui/parent/parent-home-page.js +1 -1
  46. package/install.ps1 +5 -4
  47. package/install.sh +3 -3
  48. package/package.json +7 -5
  49. package/skills/cotutor-analyze/SKILL.md +1 -1
  50. package/skills/cotutor-analyze/references//345/221/275/344/273/244/344/270/216/346/226/207/344/273/266.md +168 -10
  51. package/skills/cotutor-home/SKILL.md +1 -1
  52. package/skills/cotutor-home/references//345/221/275/344/273/244.md +75 -7
  53. package/skills/cotutor-tune/SKILL.md +8 -8
  54. package/skills/cotutor-tune/references//345/255/227/346/256/265.md +164 -10
  55. package/skills/cotutor-tutor/SKILL.md +1 -1
@@ -4,11 +4,11 @@
4
4
  * tests/skills.test.ts 断言入库的和生成的一致——改老师团时能改哪些字段、缺省是什么,永远和契约同源。SKILL.md 本体(三条路子、禁区)是手写的。
5
5
  */
6
6
  import { z } from 'zod';
7
- import { USAGE } from "../cli/usage.js";
7
+ import { commandsReference } from "../cli/usage.js";
8
8
  import { POLICY_DEFAULTS, PolicySchema, TutorSchema } from "../../schema/config.js";
9
9
  import { AGENT_FRONTMATTER_KEYS } from "../../runtime/agent-file.js";
10
10
  export const TUNE_SKILL = 'cotutor-tune';
11
- const TUNE_COMMANDS = ['add', 'add-theme', 'upgrade', 'doctor', 'pack', 'replay', 'compare', 'show'];
11
+ const TUNE_COMMANDS = ['add', 'add-theme', 'refresh', 'reset', 'doctor', 'pack', 'replay', 'compare', 'show'];
12
12
  function unwrap(s) {
13
13
  let cur = s;
14
14
  for (let i = 0; i < 5; i++) {
@@ -39,10 +39,6 @@ function rows(shape, defaults, prefix = '') {
39
39
  return out;
40
40
  }
41
41
  export function tuneReferenceDoc() {
42
- const cmds = USAGE.split('\n').filter((l) => {
43
- const m = /^\s+cotutor (\S+)/.exec(l);
44
- return m !== null && TUNE_COMMANDS.includes(m[1]);
45
- });
46
42
  // tutors 字段的缺省从 zod 的 .default() 取(enabled / hidden);display 是必需的,给个占位再抹掉
47
43
  const tutorDefaults = TutorSchema.parse({ display: 'x' });
48
44
  delete tutorDefaults.display;
@@ -68,10 +64,10 @@ ${tutorRows.join('\n')}
68
64
  |---|---|---|---|
69
65
  ${policyRows.join('\n')}
70
66
 
71
- ## 相关命令(完整用法 cotutor --help)
67
+ ## 相关命令(每条就是 cotutor <命令> --help 的内容)
72
68
 
73
69
  \`\`\`
74
- ${cmds.join('\n')}
70
+ ${commandsReference(TUNE_COMMANDS)}
75
71
  \`\`\`
76
72
  `;
77
73
  }
@@ -1,8 +1,8 @@
1
1
  import { ConfigError, UsageError } from '../shared/errors.ts';
2
- import { redactDeep, redactHome } from '../shared/home.ts';
2
+ import { expandPath, redactDeep, redactHome } from '../shared/home.ts';
3
3
  import { type RootSource, type SandboxMark, type Workspace } from '../schema/workspace.ts';
4
4
  import { type CotutorConfig } from '../schema/index.ts';
5
- export { ConfigError, UsageError, redactDeep, redactHome };
5
+ export { ConfigError, UsageError, expandPath, redactDeep, redactHome };
6
6
  export type { RootSource, Workspace };
7
7
  export declare const CONFIG_FILE = "cotutor.json";
8
8
  export declare const USER_CONFIG: string;
@@ -13,13 +13,26 @@ export interface ResolvedRoot {
13
13
  root: string;
14
14
  source: RootSource;
15
15
  }
16
- export declare function expandPath(p: string, base: string): string;
16
+ /** p 是 dir 本身或在它里面 */
17
+ export declare function isInside(p: string, dir: string): boolean;
18
+ /**
19
+ * vault 与 workspace 分开:谁也不在谁里面(2026-11-09)。vault 是家长的长期记忆,workspace 是机器的运行记录;
20
+ * 混在一起时 Obsidian 里满是 agents/、conversations/,扫 vault 要走遍对话与照片,备份恢复把家长的笔记跟着 workspace 一起盖掉。
21
+ * 旧版 init 不写 paths.vault(缺省 `.`,vault 就是 workspace 根),那样的照旧读得进来,doctor 的 vault.separate 提一句。
22
+ */
23
+ export declare const vaultSeparate: (root: string, vault: string) => boolean;
24
+ /** init 缺省的 vault:workspace 旁边的 <目录名>-vault(~/cotutor/ming → ~/cotutor/ming-vault;不在「文稿」与 iCloud 里,后台服务读得到) */
25
+ export declare const defaultVaultOf: (root: string) => string;
26
+ /** 写进 cotutor.json 的路径:家目录下的写成 ~/…(输出里不带用户名,换了用户名也对得上),别处写绝对路径 */
27
+ export declare function homePath(abs: string): string;
17
28
  /** 读 JSON:不存在 → null;坏 → ConfigError */
18
29
  export declare function readJson(file: string): unknown | null;
19
30
  /** 从 dir 向上找 cotutor.json,找到返回目录 */
20
31
  export declare function findUp(dir: string): string | null;
21
32
  /** ~/cotutor/ 下带 cotutor.json 的子目录 */
22
33
  export declare function homeWorkspaces(homeRoot?: string): string[];
34
+ /** 这台电脑上认得的孩子:~/cotutor/ 下的,加用户配置指的那个(在别处 --dir 建的) */
35
+ export declare function knownWorkspaces(): string[];
23
36
  export interface ResolveOptions {
24
37
  env?: NodeJS.ProcessEnv;
25
38
  cwd?: string;
@@ -9,24 +9,35 @@
9
9
  import { readFileSync, readdirSync, statSync } from 'node:fs';
10
10
  import { readFile, writeFile, rename } from 'node:fs/promises';
11
11
  import { ConfigError, UsageError } from "../shared/errors.js";
12
- import { redactDeep, redactHome } from "../shared/home.js";
12
+ import { expandPath, redactDeep, redactHome } from "../shared/home.js";
13
13
  import { SANDBOX_MARK, SKILLS_DIR, SandboxMarkSchema } from "../schema/workspace.js";
14
14
  import { homedir } from 'node:os';
15
- import { dirname, isAbsolute, join, resolve } from 'node:path';
15
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
16
16
  import { CotutorConfigSchema, PATH_ROLES, ROLE_DEFAULTS, WORKSPACE_ROLES, explainIssues, } from "../schema/index.js";
17
17
  // 挪走了的几样照旧从这里转出(旧的引法不用改)
18
- export { ConfigError, UsageError, redactDeep, redactHome };
18
+ export { ConfigError, UsageError, expandPath, redactDeep, redactHome };
19
19
  export const CONFIG_FILE = 'cotutor.json';
20
20
  export const USER_CONFIG = join(homedir(), '.config', 'cotutor', 'config.json');
21
21
  /** 机器级自签证书目录(cotutor cert 写 cert.pem + key.pem):证书签的是本机名与局域网 IP,与孩子无关,所以不在 workspace 里 */
22
22
  export const USER_CERT_DIR = join(homedir(), '.config', 'cotutor', 'certs');
23
23
  export const HOME_ROOT = join(homedir(), 'cotutor');
24
- export function expandPath(p, base) {
25
- if (p === '~')
26
- return homedir();
27
- if (p.startsWith('~/'))
28
- return join(homedir(), p.slice(2));
29
- return isAbsolute(p) ? p : resolve(base, p);
24
+ /** p 是 dir 本身或在它里面 */
25
+ export function isInside(p, dir) {
26
+ const r = relative(dir, p);
27
+ return r === '' || (r !== '..' && !r.startsWith(`..${sep}`) && !isAbsolute(r));
28
+ }
29
+ /**
30
+ * vault 与 workspace 分开:谁也不在谁里面(2026-11-09)。vault 是家长的长期记忆,workspace 是机器的运行记录;
31
+ * 混在一起时 Obsidian 里满是 agents/、conversations/,扫 vault 要走遍对话与照片,备份恢复把家长的笔记跟着 workspace 一起盖掉。
32
+ * 旧版 init 不写 paths.vault(缺省 `.`,vault 就是 workspace 根),那样的照旧读得进来,doctor 的 vault.separate 提一句。
33
+ */
34
+ export const vaultSeparate = (root, vault) => !isInside(vault, root) && !isInside(root, vault);
35
+ /** init 缺省的 vault:workspace 旁边的 <目录名>-vault(~/cotutor/ming → ~/cotutor/ming-vault;不在「文稿」与 iCloud 里,后台服务读得到) */
36
+ export const defaultVaultOf = (root) => join(dirname(root), `${basename(root)}-vault`);
37
+ /** 写进 cotutor.json 的路径:家目录下的写成 ~/…(输出里不带用户名,换了用户名也对得上),别处写绝对路径 */
38
+ export function homePath(abs) {
39
+ const rel = relative(homedir(), abs);
40
+ return rel && !rel.startsWith('..') && !isAbsolute(rel) ? `~/${rel.split(sep).join('/')}` : abs;
30
41
  }
31
42
  const isDir = (p) => {
32
43
  try {
@@ -84,6 +95,17 @@ export function homeWorkspaces(homeRoot = HOME_ROOT) {
84
95
  return [];
85
96
  }
86
97
  }
98
+ /** 这台电脑上认得的孩子:~/cotutor/ 下的,加用户配置指的那个(在别处 --dir 建的) */
99
+ export function knownWorkspaces() {
100
+ const roots = new Set(homeWorkspaces());
101
+ try {
102
+ const user = readJson(USER_CONFIG);
103
+ if (typeof user?.workspace === 'string' && user.workspace)
104
+ roots.add(expandPath(user.workspace, homedir()));
105
+ }
106
+ catch { /* 用户配置坏了 doctor 去报 */ }
107
+ return [...roots];
108
+ }
87
109
  export function resolveRoot(override, opts = {}) {
88
110
  const env = opts.env ?? process.env;
89
111
  const cwd = resolve(opts.cwd ?? process.cwd());
@@ -127,7 +127,7 @@ export function homeIssues(doc, ctx) {
127
127
  if (act && ctx.activities) {
128
128
  const got = ctx.activities.get(`${name}/${act.name}`);
129
129
  if (!got)
130
- out.push({ level: 'fix', line, card: n, button: k, text: b.kind === 'lecture' && !b.activity ? `${t.display}的「${b.label}」:看完怎么聊写在 agents/${name}/activities/${LECTURE_ACTIVITY}.md,这份不在(cotutor upgrade 补出厂的;或按钮下面写一行 activity: <名> 换一个)` : `${t.display}的「${b.label}」:agents/${name}/activities/${act.name}.md 不在(cotutor activity check ${name} 看这位老师有哪些)` });
130
+ out.push({ level: 'fix', line, card: n, button: k, text: b.kind === 'lecture' && !b.activity ? `${t.display}的「${b.label}」:看完怎么聊写在 agents/${name}/activities/${LECTURE_ACTIVITY}.md,这份不在(cotutor refresh 补出厂的;或按钮下面写一行 activity: <名> 换一个)` : `${t.display}的「${b.label}」:agents/${name}/activities/${act.name}.md 不在(cotutor activity check ${name} 看这位老师有哪些)` });
131
131
  else {
132
132
  if (got.fixes)
133
133
  out.push({ level: 'fix', line, card: n, button: k, text: `${t.display}的「${b.label}」:activity 文件 ${act.name}.md 有 ${got.fixes} 处要改(cotutor activity check ${name} ${act.name})` });
@@ -92,7 +92,7 @@ export async function gatherContext(ws, tutor, input, report) {
92
92
  pack.rules = TUTOR_RULES_PATH;
93
93
  }
94
94
  catch {
95
- pack.rules = `缺:${TUTOR_RULES_PATH} 不在(cotutor upgrade 补),照你老师文件里的做`;
95
+ pack.rules = `缺:${TUTOR_RULES_PATH} 不在(cotutor refresh 补),照你老师文件里的做`;
96
96
  }
97
97
  }
98
98
  pack.notes.push(...mem.notes);
@@ -186,7 +186,7 @@ async function systemParts(ws, tutor, policy, pack, act = null) {
186
186
  return { systemBody: agentBody, activityInSystem: false };
187
187
  const guide = await boardGuideOf(ws, tutor);
188
188
  if (!guide) {
189
- pack.boardGuide = `缺:${BOARD_GUIDE_PATH} 不在(cotutor upgrade 补),照你会的写`;
189
+ pack.boardGuide = `缺:${BOARD_GUIDE_PATH} 不在(cotutor refresh 补),照你会的写`;
190
190
  return { systemBody: agentBody, activityInSystem: false };
191
191
  }
192
192
  pack.boardGuide = BOARD_GUIDE_IN_SYSTEM;
@@ -1,2 +1,4 @@
1
+ /** cotutor.json 里的路径:~ 与 ~/ 是家目录,绝对的照用,相对的相对 base(从 app/workspace.ts 挪来,2026-11-09:托盘不带 zod 也要用) */
2
+ export declare function expandPath(p: string, base: string): string;
1
3
  export declare function redactHome(value: string): string;
2
4
  export declare function redactDeep<T>(value: T): T;
@@ -1,5 +1,14 @@
1
1
  /** 给人看的路径里把家目录换成 $HOME(报错、doctor、--json 输出不带出用户名;从 app/workspace.ts 挪来,2026-10-29) */
2
2
  import { homedir } from 'node:os';
3
+ import { isAbsolute, join, resolve } from 'node:path';
4
+ /** cotutor.json 里的路径:~ 与 ~/ 是家目录,绝对的照用,相对的相对 base(从 app/workspace.ts 挪来,2026-11-09:托盘不带 zod 也要用) */
5
+ export function expandPath(p, base) {
6
+ if (p === '~')
7
+ return homedir();
8
+ if (p.startsWith('~/'))
9
+ return join(homedir(), p.slice(2));
10
+ return isAbsolute(p) ? p : resolve(base, p);
11
+ }
3
12
  export function redactHome(value) {
4
13
  const home = homedir();
5
14
  return home && value.startsWith(home) ? '$HOME' + value.slice(home.length) : value;
@@ -1297,7 +1297,7 @@ ${pageDomJs(4000)}
1297
1297
  catch (e) { feedback(card, false, e.message); }
1298
1298
  } } }, '补上'))));
1299
1299
  card.append(h('ul', { class: 'gaps' }, ...gaps.map((g) => h('li', {}, h('span', { class: g.kind === 'retired' ? 'plus minus' : 'plus' }, g.kind === 'retired' ? '−' : '+'), h('span', {}, g.detail)))));
1300
- card.append(h('p', { class: 'hint' }, '新版 cotutor 带来的新老师、新运行时、命令模板里的新旗标(+),和新版不再读的出厂段(−)。cotutor.json 是你的政策文件,机器不会自己改它,所以要你点一下。只改上面这些,你改过的值一个都不动;终端里等同于 cotutor upgrade --config。'));
1300
+ card.append(h('p', { class: 'hint' }, '新版 cotutor 带来的新老师、新运行时、命令模板里的新旗标(+),和新版不再读的出厂段(−)。cotutor.json 是你的政策文件,机器不会自己改它,所以要你点一下。只改上面这些,你改过的值一个都不动;终端里等同于 cotutor refresh --config。'));
1301
1301
  return card;
1302
1302
  };
1303
1303
 
@@ -1590,7 +1590,7 @@ ${pageDomJs(4000)}
1590
1590
  const rows = listRows();
1591
1591
  const first = (want && rows.find((r) => (isTutorFile(r) ? r.tutor : r.tutor + '/' + r.name) === want)) || rows[0];
1592
1592
  if (first) return openActivity(first.tutor, first.name, true, first.kind);
1593
- $('#activity').replaceChildren(h('div', { class: 'wrap act-wrap' }, actHead(), h('p', { class: 'note' }, '这个 workspace 里还没有 activity(agents/<老师>/activities/<名>.md);cotutor upgrade 会补上出厂的。')));
1593
+ $('#activity').replaceChildren(h('div', { class: 'wrap act-wrap' }, actHead(), h('p', { class: 'note' }, '这个 workspace 里还没有 activity(agents/<老师>/activities/<名>.md);cotutor refresh 会补上出厂的。')));
1594
1594
  };
1595
1595
 
1596
1596
  const openActivity = async (t, n, force, kind) => {
@@ -1875,7 +1875,7 @@ ${pageDomJs(4000)}
1875
1875
  if (!r.diff) return [h('p', { class: 'note' }, tf ? '自家加的老师,没有出厂那份可比。' : '自家加的 activity,没有出厂那份可比。')];
1876
1876
  const out = [h('div', { class: 'srow' }, r.diff.length ? h('span', { class: 'st accent' }, '和出厂不一样 ' + r.diff.length + ' 处') : h('span', { class: 'st ok' }, '和出厂一样'), h('span', { class: 'note' }, '含没存的')),
1877
1877
  ...r.diff.map((d) => hunkEl(d, true))];
1878
- out.push(h('p', { class: 'note' }, c.state === 'upgradable' ? '这份没改过,是旧版出厂的;cotutor upgrade 会换成新的。' : c.state === 'latest' ? '存下以后就算改过的出厂件:cotutor upgrade 不再自动换新版,只报 diff。' : '改过的出厂件,cotutor upgrade 不再自动换新版,只报 diff。'));
1878
+ out.push(h('p', { class: 'note' }, c.state === 'upgradable' ? '这份没改过,是旧版出厂的;cotutor refresh 会换成新的。' : c.state === 'latest' ? '存下以后就算改过的出厂件:cotutor refresh 不再自动换新版,只报 diff。' : '改过的出厂件,cotutor refresh 不再自动换新版,只报 diff。'));
1879
1879
  out.push(rule(), h('div', { class: 'srow' },
1880
1880
  h('button', { class: 'btn danger', type: 'button', disabled: !r.diff.length && !dirtyCount(), on: { click: restoreActivityBtn } }, '恢复出厂'),
1881
1881
  h('span', { class: 'note' }, c.state === 'latest' ? '磁盘上那份就是出厂的,恢复只是丢掉没存的改动。' : '换回出厂那份;你这份存成 ' + bakName(c) + '。' + (tf ? '别的老师、这位老师的 activity 都不碰。' : '别的 activity 不碰。'))));
@@ -1920,7 +1920,7 @@ ${pageDomJs(4000)}
1920
1920
  out.push(h('div', { class: 'srow' }, h('span', { class: 'note', style: 'flex:1;min-width:160px' }, '这就是新话题里老师的系统提示:老师文件去掉 frontmatter' + (pv.board ? ',后面再接板书写法' : '') + '。'), h('button', { class: 'btn small', type: 'button', on: { click: (e) => copy(pv.text, e.target) } }, '复制全文')));
1921
1921
  if (pv.rules) {
1922
1922
  out.push(rule(), h('h3', {}, '话题第一条还带守则'),
1923
- h('p', { class: 'note' }, '有脸的老师共用的 ' + pv.rules.path + ',机器件,cotutor upgrade 每次覆盖,这里不改。这位老师要不一样的做法,写进上面的老师文件,它优先。'),
1923
+ h('p', { class: 'note' }, '有脸的老师共用的 ' + pv.rules.path + ',机器件,cotutor refresh 每次覆盖,这里不改。这位老师要不一样的做法,写进上面的老师文件,它优先。'),
1924
1924
  h('details', {}, h('summary', {}, '看守则原文'), h('pre', { class: 'pv tool' }, pv.rules.text)));
1925
1925
  }
1926
1926
  out.push(rule(), h('p', { class: 'note' }, '档案、日记、计划这些每轮现拼进上下文包,不在这里;看一轮会拼成什么(不花钱):'),
@@ -1965,7 +1965,7 @@ ${pageDomJs(4000)}
1965
1965
  const restoreActivityBtn = async () => {
1966
1966
  const c = act.cur;
1967
1967
  const tf = isTutorFile(c);
1968
- const lead = c.state === 'latest' ? '磁盘上那份就是出厂的;没存的 ' + dirtyCount() + ' 处丢掉。' : '你这份存成 ' + bakName(c) + (dirtyCount() ? ',没存的 ' + dirtyCount() + ' 处一起丢掉' : '') + '。和 cotutor upgrade --force ' + c.tutor + (tf ? '' : '/' + c.name) + ' 同一条路,' + (tf ? '别的老师不碰。' : '别的 activity 不碰。');
1968
+ const lead = c.state === 'latest' ? '磁盘上那份就是出厂的;没存的 ' + dirtyCount() + ' 处丢掉。' : '你这份存成 ' + bakName(c) + (dirtyCount() ? ',没存的 ' + dirtyCount() + ' 处一起丢掉' : '') + '。和 cotutor reset ' + c.tutor + (tf ? '' : '/' + c.name) + ' 同一条路,' + (tf ? '别的老师不碰。' : '别的 activity 不碰。');
1969
1969
  if (!(await ask('换回出厂那份?', [lead], '恢复出厂', '算了'))) return;
1970
1970
  let r;
1971
1971
  try { r = await api('POST', docUrl(c) + '/restore', {}); } catch (e) { alert(e.message); return; }
@@ -337,7 +337,7 @@ ${pageDomJs(3200)}
337
337
  if (st.ready) rows.push(h('div', { class: 'it' }, h('span', { class: 'tx' }, h('b', {}, '配好了'), h('small', {}, (st.keyTail ? 'key …' + st.keyTail + ' · 业务空间 ' + st.workspaceId : '测试用的假百炼'))),
338
338
  h('button', { type: 'button', class: 'x', disabled: BL.busy || null, on: { click: blTest } }, BL.busy && !BL.open ? '在试…' : '试连'),
339
339
  st.canEdit ? h('button', { type: 'button', class: 'x', on: { click: () => { BL.open = !BL.open; BL.msg = ''; blDraw(); } } }, BL.open ? '收起' : '换一把') : null));
340
- else rows.push(h('div', { class: 'box' }, h('b', {}, '还没配,孩子那边打不开'), h('div', { class: 'note', style: 'padding:0' }, '老师开口和想讲稿都走百炼,要百炼「按量付费」的 API key(控制台「密钥管理」里创建)和业务空间 id(控制台的业务空间详情里看);Token Plan 的 sk-sp- 不行。存在这台电脑上,不进孩子的文件夹。')));
340
+ else rows.push(h('div', { class: 'box' }, h('b', {}, '还没配,孩子那边打不开'), h('div', { class: 'note', style: 'padding:0' }, '老师开口和想讲稿都走百炼,要百炼「按量付费」的 API key(控制台「密钥管理」里创建)和业务空间 id(控制台的业务空间详情里看);Token Plan 的 sk-sp- 不行。存在这台电脑上,不进孩子的 cotutor 文件夹。')));
341
341
  if (!st.ready || BL.open) {
342
342
  if (!st.canEdit) rows.push(h('div', { class: 'box' }, h('div', { class: 'note', style: 'padding:0' }, '先设家长密码:在电脑上跑 cotutor pin;或者在这台电脑上打开家长端来配。')));
343
343
  else rows.push(h('div', { class: 'box' }, h('label', { class: 'lbl', for: 'bl-key' }, 'API key'), blKey, h('label', { class: 'lbl', for: 'bl-ws' }, '业务空间 id'), blWs,
package/install.ps1 CHANGED
@@ -1,9 +1,10 @@
1
1
  # cotutor 一行安装(Windows,在 PowerShell 里粘贴):
2
+ # & ([scriptblock]::Create([Text.Encoding]::UTF8.GetString((iwr -useb https://cdn.jsdelivr.net/npm/cotutor@latest/install.ps1).Content)))
3
+ # (jsDelivr 不标编码,PowerShell 5.1 的 irm 会把中文按 Latin-1 读成乱码,所以自己按 UTF-8 解)
4
+ # 连不上 jsDelivr 就从 GitHub 取(GitHub 标了 utf-8,irm 就行):
2
5
  # irm https://raw.githubusercontent.com/TarsLab/cotutor/main/install.ps1 | iex
3
- # 连不上 GitHub 就换这个:
4
- # irm https://cdn.jsdelivr.net/npm/cotutor@latest/install.ps1 | iex
5
- # 顺手建好孩子的文件夹、装好后台服务(短名 称呼 年级):
6
- # & ([scriptblock]::Create((irm https://raw.githubusercontent.com/TarsLab/cotutor/main/install.ps1))) ming 小明 2
6
+ # 顺手建好孩子的文件夹、装好后台服务(短名 称呼 年级),最后接上就行:
7
+ # & ([scriptblock]::Create([Text.Encoding]::UTF8.GetString((iwr -useb https://cdn.jsdelivr.net/npm/cotutor@latest/install.ps1).Content))) ming 小明 2
7
8
  #
8
9
  # 只做这几件事:查 Node 的版本;npm 连不上就用国内镜像;装 cotutor。
9
10
  # 不要管理员,不改执行策略:npm、cotutor 都用 .cmd 跑(PowerShell 缺省策略会拦 .ps1)。Node 没装或太旧时只说怎么装,不替你装。
package/install.sh CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/bin/sh
2
2
  # cotutor 一行安装(macOS、Linux):
3
- # curl -fsSL https://raw.githubusercontent.com/TarsLab/cotutor/main/install.sh | sh
4
- # 连不上 GitHub 就换这个:
5
3
  # curl -fsSL https://cdn.jsdelivr.net/npm/cotutor@latest/install.sh | sh
4
+ # 连不上 jsDelivr 就从 GitHub 取:
5
+ # curl -fsSL https://raw.githubusercontent.com/TarsLab/cotutor/main/install.sh | sh
6
6
  # 顺手建好孩子的文件夹、装好后台服务(短名 称呼 年级):
7
- # curl -fsSL https://raw.githubusercontent.com/TarsLab/cotutor/main/install.sh | sh -s -- ming 小明 2
7
+ # curl -fsSL https://cdn.jsdelivr.net/npm/cotutor@latest/install.sh | sh -s -- ming 小明 2
8
8
  #
9
9
  # 只做这几件事:查 Node 的版本;npm 连不上就用国内镜像;全局目录写不了就换到自己家目录下(不用 sudo);装 cotutor。
10
10
  # 不用 sudo,不改系统设置。Node 没装或太旧时只说怎么装,不替你装。
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "cotutor",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "releaseNotes": [
5
- "装得快多了:安装从 275 个包变成 15 个,Windows 上第一次装从 3 分钟左右变成 1 分钟左右",
6
- "平板第一次用:在家长端「扫码打开」点「装证书」,用平板相机扫一下就能装根证书",
7
- "电脑关了或睡着时,孩子在平板上打开看到「老师们还没到」,不再是浏览器的报错;电脑好了自己回来",
8
- "有了一行安装命令(Mac、Windows 都有),README 里也写了怎么卸载"
5
+ "让 AI 助手帮你装更省事:不用输网址,对它说「帮我在这台电脑上装 cotutor,先跑 npx cotutor@latest guide,照它说的一步步做」",
6
+ "孩子的笔记文件夹(档案、日记、老师的记忆)新装时放在 cotutor 文件夹旁边,也可以用你已有的 Obsidian 库;以前装的照旧能用",
7
+ "Linux 上的后台服务能起来了",
8
+ "命令更好懂:cotutor --help 只列命令,每条的用法和例子在它自己的帮助里;写错的选项会直接说哪里不对,不再悄悄忽略",
9
+ "换新出厂文件改叫 cotutor refresh,把改过的老师换回出厂的用 cotutor reset;升级还是 cotutor update"
9
10
  ],
10
11
  "description": "AI tutors that co-teach with parents at home: real-time voice, an interactive whiteboard, and learning memory in your own Obsidian vault. Self-hosted; built for China's elementary curriculum.",
11
12
  "type": "module",
@@ -58,6 +59,7 @@
58
59
  "cards",
59
60
  "skills",
60
61
  "README.md",
62
+ "INSTALL.md",
61
63
  "LICENSE",
62
64
  "NOTICE",
63
65
  "install.sh",
@@ -5,7 +5,7 @@ description: 在 cotutor workspace 里分析 AI 老师的对话:某一轮传了
5
5
 
6
6
  <!-- 给人看
7
7
  谁读:家长在 workspace 根开的编程助手
8
- 能改:机器件,upgrade 覆盖;references/命令与文件.md 由 src/app/skills/analyze-doc.ts 生成
8
+ 能改:机器件,refresh 覆盖;references/命令与文件.md 由 src/app/skills/analyze-doc.ts 生成
9
9
  改了:在试用 workspace 里开编程助手问一句「老师为什么…」(《写提示词.md》§一)
10
10
  -->
11
11
 
@@ -1,17 +1,175 @@
1
1
  # 命令与文件(机器生成,别改)
2
2
 
3
- ## 分析用的命令(在 workspace 根跑;完整用法 cotutor --help)
3
+ ## 分析用的命令(在 workspace 根跑;每条就是 cotutor <命令> --help 的内容)
4
4
 
5
5
  ```
6
- cotutor doctor [--workspace <dir>] [--json] [--live] [--fix] 逐项体检;--live 真连一次百炼实时模型、真合成一句配音(花一分钱)把 API 层的坑摆出来(真跑老师用 cotutor send);--json 里不过的项带 cmd(能直接跑)或 human(要人来);--fix 把带 cmd 的一条条修掉,要人来的留着
7
- cotutor send <老师> [<消息>] [--from kid|system] [--runtime <名>] [--new] [--photo a.jpg,b.png] [--activity <名> [--param '名=值;名=值']] [--lane main,tts] [--quiet] 终端里发一条,现场按道打印每道工序的事件,说完打印结果(孩子的话照实时写,同孩子端:上下文包带 live:、不补提问卡,只是没有嘴;--from system 不算;--new 开新话题;--photo 带作业照片,拷进 captures/,有照片时消息可以空;--activity 像孩子点了首页的 activity 按钮,新开话题,参数同按钮下面那几行;--quiet 只要结果)
8
- cotutor pack <老师> [<消息>] [--from kid|system] [--at <ISO时间>] [--activity <名> [--param '名=值;名=值']] [--json] 干跑上下文包(--activity 看 activity 话题第一条老师拿到什么):不起模型,打印现在会发给老师的那份 + 每段来自哪个文件、那里一共几条、按政策带了几条(改了档案 / 日记 / 计划立刻看效果)
9
- cotutor replay <老师> <job> [<日期>] [--runtime <名>] [--json] 回放一轮:同一问按现在的 vault 与提示词再跑一遍(落 evals/,不进孩子的对话、不配音),跑完并排打印上下文包 / 讲稿 / 卡 / 读了什么的 diff
10
- cotutor compare <老师> [<evalJob>] [<日期>] [--json] 再看一次回放的对照(不给 evalJob 就列这天回放过哪些)
11
- cotutor show <老师> <job> [<日期>] [--json] [--evals] [--workspace <dir>] 看一轮:问了什么、上下文包、当时的老师文件与技能 hash、讲稿与卡、读了什么、token 与用时、给家长的尾巴;--json 是家长端「看原文」同一份数据,给编程助手分析用
12
- cotutor trace <老师> <job> [<日期>] [--lane …] [--workspace <dir>] 回放一轮的事件(<日期>.<job>.events.jsonl;排查昨天那轮用)
13
- cotutor rate <老师> <话题> <1-5> [--date <日期>] 给一个话题打星(与家长端同一条路;≥ vault.keepScore 的话题记账时摘要才进日记)
14
- cotutor bookkeep <老师> [--date <日期>] [--thread <话题>]... 记账:这天每个还没记过的话题各起一轮记账任务,老师回「## 记账」,应用写进 vault 的日记(话题名、孩子问、摘要、观察)
6
+ 这些命令都认 --workspace <目录>(在 workspace 根跑可以不写)和 --sandbox(在试用副本里跑,不进孩子的记录)。
7
+
8
+ cotutor show — 看一轮:问了什么、上下文包、讲稿与卡、读了什么
9
+
10
+ 用法:
11
+ cotutor show <老师> <job> [<日期>] [--evals] [--json]
12
+
13
+ 选项:
14
+ --evals 看回放的那轮(evals/ 里的)
15
+ --json 机器可读的输出
16
+
17
+ 说明:
18
+ - 日期缺省今天;--json 和家长端「看原文」是同一份
19
+
20
+ 例子:
21
+ cotutor show math-tutor 1620-1 2026-09-12 --json
22
+
23
+ cotutor pack — 干跑上下文包:不起模型,看老师会拿到什么
24
+
25
+ 用法:
26
+ cotutor pack <老师> [<消息>] [--activity <名> [--param '名=值;名=值']] [--from kid|system] [--at <ISO时间>] [--json]
27
+
28
+ 选项:
29
+ --activity <名> 像孩子点了首页的 activity 按钮,新开话题
30
+ --param '名=值;名=值' activity 的参数(按钮下面那几行)
31
+ --from kid|system 谁说的(缺省 kid;system 不算孩子说的)
32
+ --at <ISO时间> 当作这个时间
33
+ --json 机器可读的输出
34
+
35
+ 说明:
36
+ - 每段来自哪个文件、一共几条、按政策带了几条;改了档案、日记、计划立刻看效果
37
+
38
+ 例子:
39
+ cotutor pack math-tutor "讲讲退位"
40
+
41
+ cotutor replay — 同一问按现在的提示词再跑一遍,并排对照
42
+
43
+ 用法:
44
+ cotutor replay <老师> <job> [<日期>] [--runtime <名>] [--json]
45
+
46
+ 选项:
47
+ --runtime <名> 换个运行时
48
+ --json 机器可读的输出
49
+
50
+ 说明:
51
+ - 落 evals/,不进孩子的对话、不配音
52
+
53
+ 注意:
54
+ - 花钱
55
+
56
+ 例子:
57
+ cotutor replay math-tutor 1620-1 2026-09-12
58
+
59
+ cotutor compare — 再看一次回放的对照
60
+
61
+ 用法:
62
+ cotutor compare <老师> [<evalJob>] [<日期>] [--json]
63
+
64
+ 选项:
65
+ --json 机器可读的输出
66
+
67
+ 说明:
68
+ - 不给 evalJob 就列这天回放过哪些
69
+
70
+ 例子:
71
+ cotutor compare math-tutor
72
+
73
+ cotutor trace — 回放一轮的事件
74
+
75
+ 用法:
76
+ cotutor trace <老师> <job> [<日期>] [--lane <道>] [--json]
77
+
78
+ 选项:
79
+ --lane <道> 只看这几道的事件,逗号分开,如 main,tts
80
+ --json 机器可读的输出
81
+
82
+ 说明:
83
+ - 读 <日期>.<job>.events.jsonl;日期缺省今天
84
+
85
+ 例子:
86
+ cotutor trace math-tutor 1620-1 --lane main
87
+
88
+ cotutor doctor — 体检;--fix 修掉能自动修的
89
+
90
+ 用法:
91
+ cotutor doctor [--json]
92
+ cotutor doctor --fix
93
+ cotutor doctor --live
94
+
95
+ 选项:
96
+ --fix 修掉能自动修的(幂等,不碰孩子的数据,不用人输东西),要人来的留着
97
+ --live 真连一次百炼的嘴、真配一句音
98
+ --json 机器可读的输出
99
+
100
+ 说明:
101
+ - --json 里没过的项带 cmd(能直接跑)或 human(要人来)
102
+ - 有必需项没过,退出码 1
103
+
104
+ 注意:
105
+ - --live 花一分钱
106
+
107
+ 例子:
108
+ cotutor doctor --fix
109
+ cotutor doctor --json
110
+
111
+ cotutor send — 终端里给老师发一条
112
+
113
+ 用法:
114
+ cotutor send <老师> [<消息>] [--new] [--photo <图,图>] [--quiet] [--json]
115
+ cotutor send <老师> --activity <名> [--param '名=值;名=值']
116
+
117
+ 选项:
118
+ --new 开新话题
119
+ --photo <图,图> 带作业照片(jpg / png,最多 9 张);有照片时消息可以空
120
+ --activity <名> 像孩子点了首页的 activity 按钮,新开话题
121
+ --param '名=值;名=值' activity 的参数(按钮下面那几行)
122
+ --from kid|system 谁说的(缺省 kid;system 不算孩子说的)
123
+ --runtime <名> 换个运行时
124
+ --lane <道> 只看这几道的事件,逗号分开,如 main,tts
125
+ --quiet 只要结果,不打印过程
126
+ --json 机器可读的输出
127
+
128
+ 说明:
129
+ - 孩子的话照实时写(同孩子端),只是没有嘴;事件落 events.jsonl,事后 cotutor trace 看
130
+
131
+ 注意:
132
+ - 花钱
133
+ - 写进孩子的对话记录(试一下用 --sandbox)
134
+
135
+ 例子:
136
+ cotutor send math-tutor "这一步为什么要借位"
137
+
138
+ cotutor rate — 给一个话题打星(1–5)
139
+
140
+ 用法:
141
+ cotutor rate <老师> <话题> <1-5> [--date <日期>] [--json]
142
+
143
+ 选项:
144
+ --date <日期> 哪天(YYYY-MM-DD,缺省今天)
145
+ --json 机器可读的输出
146
+
147
+ 说明:
148
+ - 和家长端同一条路;到 vault.keepScore 星的话题,记账时摘要才进日记
149
+
150
+ 例子:
151
+ cotutor rate math-tutor 1620-1 4
152
+
153
+ cotutor bookkeep — 记账:把这天的话题写进日记
154
+
155
+ 用法:
156
+ cotutor bookkeep <老师> [--date <日期>] [--thread <话题>] [--quiet] [--json]
157
+
158
+ 选项:
159
+ --date <日期> 哪天(YYYY-MM-DD,缺省今天)
160
+ --thread <话题> 只记这个话题
161
+ --quiet 不打印过程
162
+ --json 机器可读的输出
163
+
164
+ 说明:
165
+ - 每个还没记过的话题起一轮记账,应用写进笔记文件夹的日记
166
+
167
+ 注意:
168
+ - 花钱
169
+ - 写孩子的笔记文件夹
170
+
171
+ 例子:
172
+ cotutor bookkeep math-tutor
15
173
  ```
16
174
 
17
175
  ## 一轮的文件(conversations/ 下;回放的在 evals/ 下,同一套名字)
@@ -5,7 +5,7 @@ description: 排 cotutor 孩子端的首页。孩子学完之后,家长在 works
5
5
 
6
6
  <!-- 给人看
7
7
  谁读:家长在 workspace 根开的编程助手
8
- 能改:机器件,upgrade 覆盖;references/ 下的首页语法.md、命令.md、tutor.md 由 src/app/skills/home-doc.ts 与 cards/tutor/card.md 生成
8
+ 能改:机器件,refresh 覆盖;references/ 下的首页语法.md、命令.md、tutor.md 由 src/app/skills/home-doc.ts 与 cards/tutor/card.md 生成
9
9
  改了:在试用 workspace 里开编程助手说一句「总结一下今天」(《写提示词.md》§一)
10
10
  -->
11
11
 
@@ -1,14 +1,82 @@
1
1
  # 命令(机器生成,别改)
2
2
 
3
- 在 workspace 根跑;完整用法 cotutor --help。
3
+ 在 workspace 根跑;每条就是 cotutor <命令> --help 的内容。
4
4
 
5
5
  ```
6
- cotutor show <老师> <job> [<日期>] [--json] [--evals] [--workspace <dir>] 看一轮:问了什么、上下文包、当时的老师文件与技能 hash、讲稿与卡、读了什么、token 与用时、给家长的尾巴;--json 是家长端「看原文」同一份数据,给编程助手分析用
7
- cotutor rate <老师> <话题> <1-5> [--date <日期>] 给一个话题打星(与家长端同一条路;≥ vault.keepScore 的话题记账时摘要才进日记)
8
- cotutor bookkeep <老师> [--date <日期>] [--thread <话题>]... 记账:这天每个还没记过的话题各起一轮记账任务,老师回「## 记账」,应用写进 vault 的日记(话题名、孩子问、摘要、观察)
9
- cotutor home check [--published] [--json] 检查首页草稿 home/draft.md(或已发布的那份):孩子会看到哪几张卡、每位老师的按钮、要改的与提醒(带行号);不花钱
10
- cotutor home publish [--force] [--from <文件>] [--json] 发布首页:检查 → 留历史 home/history/<id>.md → 写 home/published.json(孩子端只读它);有要改的不发,--force 丢掉那几张照发;--from 发另一份(如历史)
11
- cotutor home show [--json] 现在发布的是哪份、几天前、每个按钮被点了几次(哪天、哪个话题)
6
+ 这些命令都认 --workspace <目录>(在 workspace 根跑可以不写)和 --sandbox(在试用副本里跑,不进孩子的记录)。
7
+
8
+ cotutor home — 检查和发布孩子端首页
9
+
10
+ 用法:
11
+ cotutor home check [--published] [--json]
12
+ cotutor home publish [--force] [--from <文件>] [--json]
13
+ cotutor home show [--json]
14
+
15
+ 选项:
16
+ --published [check] 查已发布的那份(缺省查草稿 home/draft.md)
17
+ --force [publish] 有要改的卡,丢掉它们照发
18
+ --from <文件> [publish] 发另一份(比如历史里的)
19
+ --json 机器可读的输出
20
+
21
+ 说明:
22
+ - check 不花钱:孩子会看到哪几张卡、每位老师的按钮、要改的(带行号)
23
+ - publish 先检查,留历史 home/history/,写 home/published.json;孩子端刷新就是新的
24
+ - show:现在发布的是哪份、每个按钮被点了几次
25
+
26
+ 例子:
27
+ cotutor home check
28
+ cotutor home publish
29
+
30
+ cotutor show — 看一轮:问了什么、上下文包、讲稿与卡、读了什么
31
+
32
+ 用法:
33
+ cotutor show <老师> <job> [<日期>] [--evals] [--json]
34
+
35
+ 选项:
36
+ --evals 看回放的那轮(evals/ 里的)
37
+ --json 机器可读的输出
38
+
39
+ 说明:
40
+ - 日期缺省今天;--json 和家长端「看原文」是同一份
41
+
42
+ 例子:
43
+ cotutor show math-tutor 1620-1 2026-09-12 --json
44
+
45
+ cotutor rate — 给一个话题打星(1–5)
46
+
47
+ 用法:
48
+ cotutor rate <老师> <话题> <1-5> [--date <日期>] [--json]
49
+
50
+ 选项:
51
+ --date <日期> 哪天(YYYY-MM-DD,缺省今天)
52
+ --json 机器可读的输出
53
+
54
+ 说明:
55
+ - 和家长端同一条路;到 vault.keepScore 星的话题,记账时摘要才进日记
56
+
57
+ 例子:
58
+ cotutor rate math-tutor 1620-1 4
59
+
60
+ cotutor bookkeep — 记账:把这天的话题写进日记
61
+
62
+ 用法:
63
+ cotutor bookkeep <老师> [--date <日期>] [--thread <话题>] [--quiet] [--json]
64
+
65
+ 选项:
66
+ --date <日期> 哪天(YYYY-MM-DD,缺省今天)
67
+ --thread <话题> 只记这个话题
68
+ --quiet 不打印过程
69
+ --json 机器可读的输出
70
+
71
+ 说明:
72
+ - 每个还没记过的话题起一轮记账,应用写进笔记文件夹的日记
73
+
74
+ 注意:
75
+ - 花钱
76
+ - 写孩子的笔记文件夹
77
+
78
+ 例子:
79
+ cotutor bookkeep math-tutor
12
80
  ```
13
81
 
14
82
  ## 文件