@apprun/shiqu 0.6.5 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/NOTICE +5 -2
  2. package/README.md +71 -106
  3. package/data/export-fonts/NotoSansCJKsc-Regular.otf +0 -0
  4. package/data/export-fonts/OFL.txt +92 -0
  5. package/data/export-fonts/README.md +8 -0
  6. package/dist/client/assets/{es-B0vSJwvP.js → es-uh-GOicn.js} +1 -1
  7. package/dist/client/assets/index-BIsu-Bxs.js +17 -0
  8. package/dist/client/assets/index-DRwygcbT.css +1 -0
  9. package/dist/client/assets/reader-pdf-B-dCuB6F.js +4 -0
  10. package/dist/client/assets/reader-png-B1iE7LNk.js +1 -0
  11. package/dist/client/assets/reader-zip-kwzPi4VM.js +16 -0
  12. package/dist/client/index.html +2 -2
  13. package/dist/client/offline.html +3 -1
  14. package/dist/client/sw.js +1 -1
  15. package/dist/server/cli.js +6627 -3682
  16. package/docker/seed.sql +1 -5
  17. package/drizzle/0012_generation_authorization.sql +111 -0
  18. package/drizzle/0013_reader_state_transitions.sql +132 -0
  19. package/drizzle/0014_reader_input_identity.sql +2 -0
  20. package/drizzle/0015_image_fault_checkpoints.sql +28 -0
  21. package/drizzle/0016_legacy_input_faults.sql +2 -0
  22. package/drizzle/0017_command_receipt_links.sql +51 -0
  23. package/drizzle/0018_consolidate_story_storage.sql +13 -0
  24. package/drizzle/0019_refuse_pre_consolidation_apps.sql +9 -0
  25. package/package.json +10 -1
  26. package/skills/shiqu/SKILL.md +72 -0
  27. package/dist/client/assets/index-B0URXv2Q.css +0 -1
  28. package/dist/client/assets/index-y4BxCnhg.js +0 -17
  29. package/dist/client/assets/reader-pdf-AMLMoode.js +0 -4
  30. package/dist/client/assets/reader-png-UKUNKgFB.js +0 -1
  31. package/dist/client/assets/reader-zip-CYrTAeJC.js +0 -16
package/docker/seed.sql CHANGED
@@ -1,8 +1,4 @@
1
1
  -- Initialize book metadata only. New libraries contain no finished stories.
2
- INSERT INTO books (id,title,author,era,source_url,excerpt,note) VALUES ('liaozhai','聊斋志异','蒲松龄','清','https://zh.wikisource.org/w/index.php?title=%E8%81%8A%E9%BD%8B%E5%BF%97%E7%95%B0%2F%E7%AC%AC01%E5%8D%B7&oldid=7908703#種梨','有鄉人貨梨於市,頗甘芳,價騰貴。有道士破巾絮衣,丐於車前,鄉人咄之而不去,鄉人怒,加​​以叱罵。道士曰:「一車數百顆,老衲止丐其一,於居士亦無大損,何怒為?」觀者勸置劣者一枚,令去,鄉人執不肯。
3
-
4
- 肆中傭保者,見喋聒不堪,遂出錢市一枚,付道士,道士拜謝,謂眾曰:「出家人不解吝惜,我有佳梨,請出供客。」或曰:「既有之,何不自食?」曰:「吾特需此核​​作種。」於是掬梨大啗,且盡,把核於手,解肩上鑱坎地上深數寸,納之,而覆以土,向市人索湯沃灌。好事者,於臨路店,索得沸瀋,道士接浸坎處。萬目攢視,見有勾萌出,漸大,俄成樹,枝葉扶疏,倏而花,倏而實,碩大芳馥,纍纍滿樹。道人乃即樹頭,摘賜觀者,頃刻而盡,已乃以鑱伐樹,丁丁良久乃斷,帶葉荷肩頭從容徐步而去。
5
-
6
- 初道士作法時,鄉人亦雜眾中,引領注目,竟忘其業。道士既去,始顧車中,則梨已空矣,方悟適所俵散,皆己物也。又細視車上一靶亡,是新鑿斷者,心大憤恨。急跡之,轉過牆隅,則斷靶棄垣下,始知所伐梨本,即是物也。道士不知所在,一市粲然。','已录入《种梨》原文。其他篇目需提供可靠原文后再改编。') ON CONFLICT(id) DO NOTHING;
2
+ INSERT INTO books (id,title,author,era,source_url,excerpt,note) VALUES ('liaozhai','聊斋志异','蒲松龄','清','','','待录入选定篇目的原文与来源。') ON CONFLICT(id) DO NOTHING;
7
3
  INSERT INTO books (id,title,author,era,source_url,excerpt,note) VALUES ('shishuo','世说新语','刘义庆 编','南朝宋','','','待录入选定篇目的原文与来源。') ON CONFLICT(id) DO NOTHING;
8
4
  INSERT INTO books (id,title,author,era,source_url,excerpt,note) VALUES ('zhuangzi','庄子','庄周等','先秦','','','待录入选定篇目的原文与来源。') ON CONFLICT(id) DO NOTHING;
@@ -0,0 +1,111 @@
1
+ -- Faults survive document checkpoints; receipts consume one user authorization atomically.
2
+ ALTER TABLE production_jobs ADD failure_id text;
3
+ ALTER TABLE production_jobs ADD recovery_id text;
4
+
5
+ CREATE TABLE reader_faults (
6
+ id text PRIMARY KEY NOT NULL,
7
+ session_id text NOT NULL,
8
+ owner_id text NOT NULL,
9
+ job_id text,
10
+ kind text NOT NULL,
11
+ page integer,
12
+ version text NOT NULL,
13
+ image text,
14
+ message text NOT NULL,
15
+ state text DEFAULT 'pending' NOT NULL,
16
+ command_id text,
17
+ created_at integer NOT NULL,
18
+ updated_at integer NOT NULL
19
+ );
20
+ CREATE INDEX reader_faults_session ON reader_faults(session_id,owner_id,state);
21
+ CREATE UNIQUE INDEX reader_faults_image ON reader_faults(session_id,owner_id,page)
22
+ WHERE kind='image-loss' AND state IN ('pending','recovering');
23
+
24
+ CREATE TABLE reader_commands (
25
+ request_id text NOT NULL,
26
+ owner_id text NOT NULL,
27
+ session_id text NOT NULL,
28
+ action text NOT NULL,
29
+ fingerprint text NOT NULL,
30
+ payload text NOT NULL,
31
+ fault_id text,
32
+ result_id text,
33
+ status text DEFAULT 'succeeded' NOT NULL,
34
+ result text,
35
+ error text,
36
+ created_at integer NOT NULL,
37
+ updated_at integer NOT NULL,
38
+ PRIMARY KEY(owner_id,request_id)
39
+ );
40
+ CREATE INDEX reader_commands_session ON reader_commands(session_id,owner_id);
41
+
42
+ -- Claim, enqueue and receipt share the INSERT's transaction, including across processes.
43
+ CREATE TRIGGER reader_recover_command AFTER INSERT ON reader_commands
44
+ WHEN NEW.action='recover' BEGIN
45
+ UPDATE reader_faults SET state='recovering',command_id=NEW.request_id,updated_at=NEW.updated_at
46
+ WHERE id=NEW.fault_id AND owner_id=NEW.owner_id AND session_id=NEW.session_id AND state='pending';
47
+ UPDATE production_jobs SET state='queued',error=NULL,interrupted=0,lease_token=NULL,lease_until=0,
48
+ attempts=0,recovery_id=NEW.request_id,
49
+ result=CASE WHEN json_extract(NEW.payload,'$.redraw')=1 THEN NULL ELSE result END,
50
+ updated_at=NEW.updated_at
51
+ WHERE session_id=NEW.session_id AND owner_id=NEW.owner_id
52
+ AND id IN (SELECT value FROM json_each(NEW.payload,'$.jobs')) AND state!='running';
53
+ UPDATE reader_sessions SET state='idle',error=NULL
54
+ WHERE id=NEW.session_id AND user_id=NEW.owner_id AND state='failed';
55
+ END;
56
+
57
+ -- Each ordinary failure has a fresh identity. An interrupted attempt may retain its identity.
58
+ CREATE TRIGGER reader_job_failed AFTER UPDATE OF state ON production_jobs
59
+ WHEN NEW.state='failed' AND (OLD.state!='failed' OR NEW.failure_id IS NOT OLD.failure_id) BEGIN
60
+ UPDATE production_jobs SET failure_id=CASE
61
+ WHEN NEW.failure_id IS NULL OR NEW.failure_id IS OLD.failure_id THEN lower(hex(randomblob(16)))
62
+ ELSE NEW.failure_id END WHERE id=NEW.id;
63
+ UPDATE reader_faults SET state='replaced',updated_at=NEW.updated_at
64
+ WHERE session_id=NEW.session_id AND owner_id=NEW.owner_id AND state='recovering'
65
+ AND (command_id=NEW.recovery_id OR job_id=NEW.id)
66
+ AND id!=(SELECT failure_id FROM production_jobs WHERE id=NEW.id);
67
+ INSERT INTO reader_faults(id,session_id,owner_id,job_id,kind,page,version,message,created_at,updated_at)
68
+ SELECT failure_id,NEW.session_id,NEW.owner_id,NEW.id,
69
+ CASE WHEN NEW.interrupted=1 THEN 'interrupted' ELSE 'job' END,
70
+ CASE WHEN NEW.kind IN ('image','image-publish') THEN json_extract(NEW.payload,'$.page') END,
71
+ NEW.version,COALESCE(NEW.error,'制作未完成'),NEW.updated_at,NEW.updated_at
72
+ FROM production_jobs WHERE id=NEW.id
73
+ ON CONFLICT(id) DO UPDATE SET state='pending',message=excluded.message,updated_at=excluded.updated_at;
74
+ UPDATE reader_commands SET status=CASE WHEN NEW.interrupted=1 AND NEW.attempts<3 THEN 'queued' ELSE 'failed' END,
75
+ error=NEW.error,result=json_object('jobId',NEW.id,'faultId',(SELECT failure_id FROM production_jobs WHERE id=NEW.id)),updated_at=NEW.updated_at
76
+ WHERE request_id=NEW.recovery_id AND owner_id=NEW.owner_id AND action='recover' AND status IN ('queued','running');
77
+ END;
78
+
79
+ CREATE TRIGGER reader_job_queued AFTER UPDATE OF state ON production_jobs
80
+ WHEN NEW.state='queued' AND OLD.state='failed' AND OLD.interrupted=1 BEGIN
81
+ UPDATE reader_faults SET state='recovering',updated_at=NEW.updated_at
82
+ WHERE id=NEW.failure_id AND session_id=NEW.session_id AND owner_id=NEW.owner_id AND state='pending';
83
+ UPDATE reader_commands SET status='queued',updated_at=NEW.updated_at
84
+ WHERE request_id=NEW.recovery_id AND owner_id=NEW.owner_id AND action='recover' AND status!='succeeded';
85
+ END;
86
+
87
+ CREATE TRIGGER reader_job_running AFTER UPDATE OF state ON production_jobs
88
+ WHEN NEW.state='running' AND OLD.state!='running' BEGIN
89
+ UPDATE reader_commands SET status='running',updated_at=NEW.updated_at
90
+ WHERE request_id=NEW.recovery_id AND owner_id=NEW.owner_id AND action='recover' AND status='queued';
91
+ END;
92
+
93
+ CREATE TRIGGER reader_job_succeeded AFTER UPDATE OF state ON production_jobs
94
+ WHEN NEW.state='succeeded' AND OLD.state!='succeeded' BEGIN
95
+ UPDATE reader_faults SET state='resolved',updated_at=NEW.updated_at
96
+ WHERE session_id=NEW.session_id AND owner_id=NEW.owner_id AND state IN ('pending','recovering')
97
+ AND (job_id=NEW.id OR command_id=NEW.recovery_id AND EXISTS (
98
+ SELECT 1 FROM reader_commands c WHERE c.owner_id=NEW.owner_id AND c.request_id=NEW.recovery_id
99
+ AND json_extract(c.payload,'$.resolutionJob')=NEW.id))
100
+ AND (command_id IS NULL OR command_id=NEW.recovery_id);
101
+ UPDATE reader_commands SET status='succeeded',error=NULL,result=NEW.result,updated_at=NEW.updated_at
102
+ WHERE request_id=NEW.recovery_id AND owner_id=NEW.owner_id AND action='recover'
103
+ AND status IN ('queued','running') AND json_extract(payload,'$.resolutionJob')=NEW.id;
104
+ END;
105
+
106
+ -- Saved pre-upgrade failures become stable, explicitly recoverable checkpoints, never new work.
107
+ UPDATE production_jobs SET failure_id=lower(hex(randomblob(16))) WHERE state='failed';
108
+ INSERT INTO reader_faults(id,session_id,owner_id,job_id,kind,page,version,message,created_at,updated_at)
109
+ SELECT failure_id,session_id,owner_id,id,CASE WHEN interrupted=1 THEN 'interrupted' ELSE 'job' END,
110
+ CASE WHEN kind IN ('image','image-publish') THEN json_extract(payload,'$.page') END,
111
+ version,COALESCE(error,'制作未完成'),updated_at,updated_at FROM production_jobs WHERE state='failed';
@@ -0,0 +1,132 @@
1
+ -- Extend command/checkpoint transitions without rewriting an applied migration.
2
+ DROP TRIGGER IF EXISTS reader_recover_command;
3
+ DROP TRIGGER IF EXISTS reader_control_command;
4
+ DROP TRIGGER IF EXISTS reader_override_command;
5
+ DROP TRIGGER IF EXISTS reader_job_deleted;
6
+ DROP TRIGGER IF EXISTS reader_job_failed;
7
+ DROP TRIGGER IF EXISTS reader_job_queued;
8
+ DROP TRIGGER IF EXISTS reader_job_running;
9
+ DROP TRIGGER IF EXISTS reader_job_succeeded;
10
+ DROP TRIGGER IF EXISTS reader_legacy_failure;
11
+ DROP TRIGGER IF EXISTS reader_intake_command;
12
+ DROP TRIGGER IF EXISTS reader_search_command;
13
+ -- Claim, enqueue and receipt share the INSERT's transaction, including across processes.
14
+ CREATE TRIGGER reader_recover_command AFTER INSERT ON reader_commands
15
+ WHEN NEW.action='recover' BEGIN
16
+ UPDATE reader_faults SET state=CASE WHEN NEW.status='succeeded' THEN 'resolved' ELSE 'recovering' END,command_id=NEW.request_id,updated_at=NEW.updated_at
17
+ WHERE id=NEW.fault_id AND owner_id=NEW.owner_id AND session_id=NEW.session_id AND state='pending';
18
+ UPDATE production_jobs SET state='queued',error=NULL,interrupted=0,lease_token=NULL,lease_until=0,
19
+ attempts=0,recovery_id=NEW.request_id,
20
+ result=CASE WHEN json_extract(NEW.payload,'$.redraw')=1 THEN NULL ELSE result END,
21
+ updated_at=NEW.updated_at
22
+ WHERE session_id=NEW.session_id AND owner_id=NEW.owner_id
23
+ AND id IN (SELECT value FROM json_each(NEW.payload,'$.jobs')) AND state!='running';
24
+ UPDATE reader_sessions SET state='idle',error=NULL
25
+ WHERE id=NEW.session_id AND user_id=NEW.owner_id AND state='failed';
26
+ END;
27
+
28
+ CREATE TRIGGER reader_control_command AFTER INSERT ON reader_commands
29
+ WHEN NEW.action='control' BEGIN
30
+ UPDATE reader_sessions SET
31
+ generation_control=CASE json_extract(NEW.payload,'$.action') WHEN 'resume' THEN 'active' WHEN 'pause' THEN 'paused' WHEN 'stop' THEN 'stopped' ELSE generation_control END,
32
+ generation_mode=CASE json_extract(NEW.payload,'$.action') WHEN 'resume' THEN 'background' WHEN 'background' THEN 'background' WHEN 'reader' THEN 'reader' ELSE generation_mode END,
33
+ document=CASE WHEN json_extract(NEW.payload,'$.action') IN ('resume','pause') THEN json_remove(document,'$.revision.handoff') ELSE document END
34
+ WHERE id=NEW.session_id AND user_id=NEW.owner_id;
35
+ END;
36
+
37
+ CREATE TRIGGER reader_override_command AFTER INSERT ON reader_commands
38
+ WHEN NEW.action='override' BEGIN
39
+ UPDATE reader_sessions SET document=json_remove(json_set(document,'$.visualReview.status','passed','$.visualReview.overridden',json('true')),'$.visualReview.stopReason'),
40
+ state='idle',error=NULL,lock_token=NULL,lock_until=0 WHERE id=NEW.session_id AND user_id=NEW.owner_id;
41
+ UPDATE reader_faults SET state='resolved',command_id=NEW.request_id,updated_at=NEW.updated_at
42
+ WHERE id=NEW.fault_id AND owner_id=NEW.owner_id AND session_id=NEW.session_id AND state='pending';
43
+ DELETE FROM production_jobs WHERE session_id=NEW.session_id AND owner_id=NEW.owner_id AND pool='text' AND kind!='discover' AND state IN ('queued','failed');
44
+ END;
45
+
46
+ CREATE TRIGGER reader_job_deleted AFTER DELETE ON production_jobs BEGIN
47
+ UPDATE reader_faults SET state='replaced' WHERE job_id=OLD.id AND owner_id=OLD.owner_id AND state IN ('pending','recovering');
48
+ END;
49
+
50
+ -- Each ordinary failure has a fresh identity. An interrupted attempt may retain its identity.
51
+ CREATE TRIGGER reader_job_failed AFTER UPDATE OF state ON production_jobs
52
+ WHEN NEW.state='failed' AND (OLD.state!='failed' OR NEW.failure_id IS NOT OLD.failure_id) BEGIN
53
+ UPDATE production_jobs SET failure_id=CASE
54
+ WHEN NEW.failure_id IS NULL OR NEW.failure_id IS OLD.failure_id THEN lower(hex(randomblob(16)))
55
+ ELSE NEW.failure_id END WHERE id=NEW.id;
56
+ UPDATE reader_faults SET state='replaced',updated_at=NEW.updated_at
57
+ WHERE session_id=NEW.session_id AND owner_id=NEW.owner_id AND state IN ('pending','recovering')
58
+ AND (command_id=NEW.recovery_id OR job_id=NEW.id)
59
+ AND id!=(SELECT failure_id FROM production_jobs WHERE id=NEW.id);
60
+ INSERT INTO reader_faults(id,session_id,owner_id,job_id,kind,page,version,image,message,created_at,updated_at)
61
+ SELECT failure_id,NEW.session_id,NEW.owner_id,NEW.id,
62
+ CASE WHEN EXISTS(SELECT 1 FROM reader_faults f WHERE f.command_id=NEW.recovery_id AND f.owner_id=NEW.owner_id AND f.kind='image-loss') THEN 'image-loss'
63
+ WHEN NEW.error LIKE '这幅图片的文件已丢失或损坏。%' THEN 'image-loss'
64
+ WHEN NEW.interrupted=1 THEN 'interrupted' ELSE 'job' END,
65
+ CASE WHEN NEW.kind='image' AND NEW.error LIKE '这幅图片的文件已丢失或损坏。%' THEN 0
66
+ WHEN NEW.kind IN ('image','image-publish') THEN json_extract(NEW.payload,'$.page') END,
67
+ NEW.version,(SELECT image FROM reader_faults f WHERE f.command_id=NEW.recovery_id AND f.owner_id=NEW.owner_id AND f.kind='image-loss' LIMIT 1),
68
+ COALESCE(NEW.error,'制作未完成'),NEW.updated_at,NEW.updated_at
69
+ FROM production_jobs WHERE id=NEW.id
70
+ ON CONFLICT(id) DO UPDATE SET state='pending',message=excluded.message,updated_at=excluded.updated_at;
71
+ UPDATE reader_commands SET status=CASE WHEN NEW.interrupted=1 AND NEW.attempts<3 THEN 'queued' ELSE 'failed' END,
72
+ error=NEW.error,result=json_object('jobId',NEW.id,'faultId',(SELECT failure_id FROM production_jobs WHERE id=NEW.id)),updated_at=NEW.updated_at
73
+ WHERE request_id=NEW.recovery_id AND owner_id=NEW.owner_id AND action='recover' AND status IN ('queued','running');
74
+ END;
75
+
76
+ CREATE TRIGGER reader_job_queued AFTER UPDATE OF state ON production_jobs
77
+ WHEN NEW.state='queued' AND OLD.state='failed' AND OLD.interrupted=1 BEGIN
78
+ UPDATE reader_faults SET state='recovering',updated_at=NEW.updated_at
79
+ WHERE id=NEW.failure_id AND session_id=NEW.session_id AND owner_id=NEW.owner_id AND state='pending';
80
+ UPDATE reader_commands SET status='queued',updated_at=NEW.updated_at
81
+ WHERE request_id=NEW.recovery_id AND owner_id=NEW.owner_id AND action='recover' AND status!='succeeded';
82
+ END;
83
+
84
+ CREATE TRIGGER reader_job_running AFTER UPDATE OF state ON production_jobs
85
+ WHEN NEW.state='running' AND OLD.state!='running' BEGIN
86
+ UPDATE reader_commands SET status='running',updated_at=NEW.updated_at
87
+ WHERE request_id=NEW.recovery_id AND owner_id=NEW.owner_id AND action='recover' AND status='queued';
88
+ END;
89
+
90
+ CREATE TRIGGER reader_job_succeeded AFTER UPDATE OF state ON production_jobs
91
+ WHEN NEW.state='succeeded' AND OLD.state!='succeeded' BEGIN
92
+ UPDATE reader_faults SET state='resolved',updated_at=NEW.updated_at
93
+ WHERE session_id=NEW.session_id AND owner_id=NEW.owner_id AND state IN ('pending','recovering')
94
+ AND (job_id=NEW.id OR command_id=NEW.recovery_id AND EXISTS (
95
+ SELECT 1 FROM reader_commands c WHERE c.owner_id=NEW.owner_id AND c.request_id=NEW.recovery_id
96
+ AND json_extract(c.payload,'$.resolutionJob')=NEW.id))
97
+ AND (command_id IS NULL OR command_id=NEW.recovery_id);
98
+ UPDATE reader_commands SET status='succeeded',error=NULL,result=NEW.result,updated_at=NEW.updated_at
99
+ WHERE request_id=NEW.recovery_id AND owner_id=NEW.owner_id AND action='recover'
100
+ AND status IN ('queued','running') AND json_extract(payload,'$.resolutionJob')=NEW.id;
101
+ END;
102
+
103
+ INSERT INTO reader_faults(id,session_id,owner_id,kind,version,message,created_at,updated_at)
104
+ SELECT lower(hex(randomblob(16))),s.id,s.user_id,'job','legacy',COALESCE(s.error,'制作未完成'),
105
+ COALESCE(CAST(strftime('%s',s.updated_at) AS integer),CAST(strftime('%s','now') AS integer))*1000,COALESCE(CAST(strftime('%s',s.updated_at) AS integer),CAST(strftime('%s','now') AS integer))*1000
106
+ FROM reader_sessions s WHERE s.state='failed' AND NOT EXISTS(
107
+ SELECT 1 FROM reader_faults f WHERE f.session_id=s.id AND f.owner_id=s.user_id AND f.state IN ('pending','recovering'));
108
+
109
+ CREATE TRIGGER reader_legacy_failure AFTER UPDATE OF state ON reader_sessions
110
+ WHEN NEW.state='failed' AND OLD.state!='failed'
111
+ AND NOT EXISTS(SELECT 1 FROM production_jobs j WHERE j.session_id=NEW.id AND j.owner_id=NEW.user_id AND j.state='running')
112
+ AND NOT EXISTS(SELECT 1 FROM reader_faults f WHERE f.session_id=NEW.id AND f.owner_id=NEW.user_id AND f.state IN ('pending','recovering')) BEGIN
113
+ INSERT INTO reader_faults(id,session_id,owner_id,kind,version,message,created_at,updated_at)
114
+ VALUES(lower(hex(randomblob(16))),NEW.id,NEW.user_id,'job','legacy',COALESCE(NEW.error,'制作未完成'),
115
+ CAST(strftime('%s','now') AS integer)*1000,CAST(strftime('%s','now') AS integer)*1000);
116
+ END;
117
+
118
+ -- Intake checkpoints and their accepted command receipts are one atomic write.
119
+ CREATE TRIGGER reader_intake_command AFTER UPDATE OF document ON reader_sessions
120
+ WHEN json_type(CASE WHEN json_valid(NEW.document) THEN NEW.document ELSE '{}' END,'$.intake.lastAction.id')='text'
121
+ AND json_extract(NEW.document,'$.intake.lastAction.id') IS NOT json_extract(CASE WHEN json_valid(OLD.document) THEN OLD.document ELSE '{}' END,'$.intake.lastAction.id') BEGIN
122
+ INSERT INTO reader_commands(request_id,owner_id,session_id,action,fingerprint,payload,result_id,created_at,updated_at)
123
+ VALUES(json_extract(NEW.document,'$.intake.lastAction.id'),NEW.user_id,NEW.id,'setup',json_extract(NEW.document,'$.intake.lastAction.hash'),'{}',NEW.id,
124
+ CAST(strftime('%s','now') AS integer)*1000,CAST(strftime('%s','now') AS integer)*1000);
125
+ END;
126
+ CREATE TRIGGER reader_search_command AFTER UPDATE OF document ON reader_sessions
127
+ WHEN json_type(CASE WHEN json_valid(NEW.document) THEN NEW.document ELSE '{}' END,'$.intake.search.commandHash')='text'
128
+ AND json_extract(NEW.document,'$.intake.search.requestId') IS NOT json_extract(CASE WHEN json_valid(OLD.document) THEN OLD.document ELSE '{}' END,'$.intake.search.requestId') BEGIN
129
+ INSERT INTO reader_commands(request_id,owner_id,session_id,action,fingerprint,payload,result_id,created_at,updated_at)
130
+ VALUES(json_extract(NEW.document,'$.intake.search.requestId'),NEW.user_id,NEW.id,'setup',json_extract(NEW.document,'$.intake.search.commandHash'),'{}',NEW.id,
131
+ CAST(strftime('%s','now') AS integer)*1000,CAST(strftime('%s','now') AS integer)*1000);
132
+ END;
@@ -0,0 +1,2 @@
1
+ -- server/reader-input-migration.ts remaps paid work and its dependencies atomically at startup.
2
+ -- No schema change and no generation, review or image request is performed by this migration.
@@ -0,0 +1,28 @@
1
+ -- The loss record and completion revocation must survive a crash as one transaction.
2
+ CREATE TRIGGER reader_image_lost AFTER INSERT ON reader_faults WHEN NEW.kind='image-loss' BEGIN
3
+ UPDATE reader_sessions SET state=CASE WHEN state='running' THEN state ELSE 'failed' END,error=NEW.message
4
+ WHERE id=NEW.session_id AND user_id=NEW.owner_id;
5
+ END;
6
+
7
+ -- A restored original is a saved result, not new drawing permission. Close its failed local/model
8
+ -- checkpoint, or requeue only the dependent call that failed before inference on its missing anchor.
9
+ CREATE TRIGGER reader_image_restored AFTER UPDATE OF state ON reader_faults
10
+ WHEN OLD.state='pending' AND NEW.state='resolved' AND NEW.kind='image-loss' BEGIN
11
+ UPDATE production_jobs SET
12
+ state=CASE WHEN CAST(json_extract(payload,'$.page') AS integer)=NEW.page THEN 'succeeded' ELSE 'queued' END,
13
+ result=CASE WHEN CAST(json_extract(payload,'$.page') AS integer)=NEW.page THEN
14
+ json_object('value',(SELECT json_extract(document,'$.images['||NEW.page||']') FROM reader_sessions WHERE id=NEW.session_id AND user_id=NEW.owner_id),'model','saved','requestId',NULL,'usage',NULL) ELSE result END,
15
+ error=NULL,interrupted=0,lease_token=NULL,lease_until=0,updated_at=NEW.updated_at
16
+ WHERE id=NEW.job_id AND session_id=NEW.session_id AND owner_id=NEW.owner_id AND state='failed'
17
+ AND kind IN ('image','image-publish')
18
+ AND (NEW.command_id IS NOT NULL OR error LIKE '这幅图片的文件已丢失或损坏。%')
19
+ AND EXISTS(SELECT 1 FROM reader_sessions s WHERE s.id=NEW.session_id AND s.user_id=NEW.owner_id
20
+ AND COALESCE(json_extract(s.document,'$.images['||NEW.page||']'),'')!='');
21
+ UPDATE reader_sessions SET state=CASE
22
+ WHEN json_type(document,'$.plan')='object' AND json_type(document,'$.original')='object'
23
+ AND (json_type(document,'$.intake') IS NULL OR json_extract(document,'$.intake.phase')='confirmed')
24
+ AND (json_type(document,'$.visualReview') IS NULL OR json_extract(document,'$.visualReview.status')='passed' OR json_extract(document,'$.visualReview.overridden')=1)
25
+ AND NOT EXISTS(SELECT 1 FROM json_each(document,'$.plan.pages') p WHERE COALESCE(json_extract(document,'$.images['||p.key||']'),'')='')
26
+ THEN 'completed' ELSE 'idle' END,error=NULL WHERE id=NEW.session_id AND user_id=NEW.owner_id AND state='failed'
27
+ AND NOT EXISTS(SELECT 1 FROM reader_faults f WHERE f.session_id=NEW.session_id AND f.owner_id=NEW.owner_id AND f.state IN ('pending','recovering'));
28
+ END;
@@ -0,0 +1,2 @@
1
+ -- Startup conservatively retains failures whose old display-name hash cannot be associated with
2
+ -- current generation input. Explicit recovery binds the durable legacy fault to current work.
@@ -0,0 +1,51 @@
1
+ -- Replacement commits occupy the same owner/request namespace atomically with the family CAS.
2
+ CREATE TRIGGER reader_replace_receipt AFTER UPDATE OF document ON reader_sessions
3
+ WHEN json_type(CASE WHEN json_valid(OLD.document) THEN OLD.document ELSE '{}' END,'$.reselection')='object'
4
+ AND json_extract(NEW.document,'$.revision.kind')='selection' BEGIN
5
+ INSERT INTO reader_commands(request_id,owner_id,session_id,action,fingerprint,payload,result_id,created_at,updated_at)
6
+ VALUES(json_extract(NEW.document,'$.revision.selectionRequest'),NEW.user_id,json_extract(NEW.document,'$.revision.parentId'),
7
+ 'replace',json_extract(NEW.document,'$.revision.requestHash'),'{}',NEW.id,
8
+ CAST(strftime('%s','now') AS integer)*1000,CAST(strftime('%s','now') AS integer)*1000);
9
+ END;
10
+ INSERT INTO reader_commands(request_id,owner_id,session_id,action,fingerprint,payload,result_id,created_at,updated_at)
11
+ SELECT json_extract(document,'$.revision.selectionRequest'),user_id,json_extract(document,'$.revision.parentId'),
12
+ 'replace',json_extract(document,'$.revision.requestHash'),'{}',id,0,0 FROM reader_sessions s
13
+ WHERE json_extract(CASE WHEN json_valid(document) THEN document ELSE '{}' END,'$.revision.kind')='selection'
14
+ AND NOT EXISTS(SELECT 1 FROM reader_commands c WHERE c.owner_id=s.user_id AND c.request_id=json_extract(s.document,'$.revision.selectionRequest')
15
+ AND c.action='replace' AND c.fingerprint=json_extract(s.document,'$.revision.requestHash'));
16
+
17
+ -- Explicit local recovery can carry an already-paid legacy result into the current graph. The
18
+ -- image stays succeeded; the grant retries only publication, and a crash cannot lose the link.
19
+ CREATE TRIGGER reader_recovery_adopt AFTER INSERT ON reader_commands
20
+ WHEN NEW.action='recover' AND json_type(NEW.payload,'$.adopt')='object' BEGIN
21
+ UPDATE production_jobs SET state='succeeded',result=json_extract(NEW.payload,'$.adopt.result'),
22
+ attempts=json_extract(NEW.payload,'$.adopt.attempts'),error=NULL,interrupted=0,updated_at=NEW.updated_at
23
+ WHERE id=json_extract(NEW.payload,'$.adopt.jobId') AND session_id=NEW.session_id AND owner_id=NEW.owner_id AND state!='running';
24
+ UPDATE reader_faults SET job_id=json_extract(NEW.payload,'$.resolutionJob'),
25
+ version=(SELECT version FROM production_jobs WHERE id=json_extract(NEW.payload,'$.resolutionJob'))
26
+ WHERE id=NEW.fault_id AND session_id=NEW.session_id AND owner_id=NEW.owner_id;
27
+ END;
28
+ -- When a legacy paid publication is discovered lost, its fresh failure keeps the preserved link.
29
+ CREATE TRIGGER reader_legacy_fault_lineage AFTER INSERT ON reader_faults
30
+ WHEN NEW.job_id IS NOT NULL AND EXISTS(SELECT 1 FROM reader_faults old WHERE old.job_id=NEW.job_id
31
+ AND old.owner_id=NEW.owner_id AND old.version='legacy' AND old.id!=NEW.id) BEGIN
32
+ UPDATE reader_faults SET version='legacy' WHERE id=NEW.id;
33
+ END;
34
+ -- A new pre-confirmation selection/preparation generation retires failures of the old input,
35
+ -- including unmanaged legacy faults that have no queue row for the normal prune hook.
36
+ CREATE TRIGGER reader_intake_fault_generation AFTER UPDATE OF document ON reader_sessions
37
+ WHEN json_extract(CASE WHEN json_valid(OLD.document) THEN OLD.document ELSE '{}' END,'$.intake.generation') IS NOT NULL
38
+ AND json_extract(OLD.document,'$.intake.phase')!='confirmed'
39
+ AND json_extract(NEW.document,'$.intake.generation')!=json_extract(OLD.document,'$.intake.generation') BEGIN
40
+ UPDATE reader_faults SET state='replaced' WHERE session_id=NEW.id AND owner_id=NEW.user_id AND state IN ('pending','recovering');
41
+ END;
42
+
43
+ -- Cross-table occupancy is checked under the same SQLite write lock, after any racing pre-read.
44
+ CREATE TRIGGER reader_command_id_guard BEFORE INSERT ON reader_commands
45
+ WHEN EXISTS(SELECT 1 FROM reader_sessions s WHERE s.id=NEW.request_id AND s.user_id=NEW.owner_id) BEGIN
46
+ SELECT RAISE(ABORT,'reader_request_id_conflict');
47
+ END;
48
+ CREATE TRIGGER reader_session_id_guard BEFORE INSERT ON reader_sessions
49
+ WHEN EXISTS(SELECT 1 FROM reader_commands c WHERE c.request_id=NEW.id AND c.owner_id=NEW.user_id) BEGIN
50
+ SELECT RAISE(ABORT,'reader_request_id_conflict');
51
+ END;
@@ -0,0 +1,13 @@
1
+ -- The coordinator inventories files and verifies recovery before this destructive transaction.
2
+ DELETE FROM book_image_references WHERE story_id NOT IN (SELECT id FROM reader_sessions);
3
+ DELETE FROM events WHERE story_id NOT IN (SELECT id FROM reader_sessions);
4
+ DELETE FROM library_deletions WHERE kind='published';
5
+ -- liaozhai returns to the seed placeholder so upgraded and new libraries hold the same catalog row.
6
+ UPDATE books SET excerpt='',source_url='',note=CASE id WHEN 'liaozhai' THEN '待录入选定篇目的原文与来源。' ELSE '' END WHERE id IN ('liaozhai','zhongshanlang');
7
+ DELETE FROM books WHERE id='zhongshanlang' AND NOT EXISTS (
8
+ SELECT 1 FROM reader_sessions WHERE json_extract(selection,'$.bookId')='zhongshanlang');
9
+ DROP TABLE stories;
10
+ DROP TABLE jobs;
11
+ DROP TABLE progress;
12
+ ALTER TABLE reader_sessions RENAME TO stories;
13
+ PRAGMA user_version=1;
@@ -0,0 +1,9 @@
1
+ -- Released 0.2-0.7 apps have no schema-version check, but each one seeds liaozhai first with its
2
+ -- source URL and demo text on every open. Abort that write so they refuse this library with a clear
3
+ -- message instead of opening it and failing every story request. The 0.8+ seed row is empty.
4
+ -- 0.7 CLI receipts show only EXPORT_/BACKUP_/SKILL_ messages, so the code uses the BACKUP_ prefix.
5
+ CREATE TRIGGER books_refuse_pre_consolidation_seed BEFORE INSERT ON books
6
+ WHEN NEW.id='liaozhai' AND (NEW.source_url<>'' OR NEW.excerpt<>'')
7
+ BEGIN
8
+ SELECT RAISE(ABORT,'BACKUP_APP_TOO_OLD: this library requires Shiqu 0.8.0 or later; update the app, or restore the pre-upgrade backup with its matching older app');
9
+ END;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apprun/shiqu",
3
- "version": "0.6.5",
3
+ "version": "0.8.0",
4
4
  "description": "Turn Chinese classics into illustrated stories in a local browser app.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Yiyi Sun",
@@ -22,6 +22,8 @@
22
22
  "dist/client",
23
23
  "dist/server/cli.js",
24
24
  "data/library",
25
+ "data/export-fonts",
26
+ "skills/shiqu",
25
27
  "drizzle/*.sql",
26
28
  "docker/seed.sql",
27
29
  ".env.example",
@@ -34,9 +36,16 @@
34
36
  "registry": "https://registry.npmjs.org/"
35
37
  },
36
38
  "dependencies": {
39
+ "@jsquash/jpeg": "1.6.0",
40
+ "@jsquash/png": "3.1.1",
41
+ "@jsquash/webp": "1.5.0",
42
+ "@napi-rs/canvas": "1.0.13",
43
+ "@noble/hashes": "2.4.0",
37
44
  "drizzle-orm": "0.45.2",
45
+ "fflate": "0.8.2",
38
46
  "htmlparser2": "10.0.0",
39
47
  "opencc-js": "1.0.5",
48
+ "pdf-lib": "1.17.1",
40
49
  "qrcode-terminal": "0.12.0",
41
50
  "tiktoken": "1.0.22",
42
51
  "zod": "^3.25.76"
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: shiqu
3
+ description: 根据拾趣的数据契约与业务规则,自主编写任务程序,完成故事选题、制作、编辑、书库分析和批量整理,并预览、导出或维护作品。适用于明确使用拾趣的任务。
4
+ ---
5
+
6
+ # 拾趣任务契约
7
+
8
+ 根据用户目标和已保存状态决定操作,自主编写并运行本次任务需要的程序。筛选、分组、比较、报告和批量调度由你实现,不必为每种任务寻找专用命令;简单任务可以直接调用现有操作。
9
+
10
+ 程序通过拾趣现有生产入口读取、保存和执行,不重写状态机、模型调用、审核、版本管理、媒体编码或导出逻辑,不直接改 SQLite 或媒体文件。当前公开入口是 CLI 子命令;没有公开 SDK,不依赖 npm 缓存路径或未导出的内部模块。无需启动 HTTP 服务、桌面或前端;不带子命令会启动浏览器服务。
11
+
12
+ ## 执行环境与协议
13
+
14
+ - 使用 Node 24。源码项目沿用 `pnpm cli`,已安装命令沿用 `shiqu`,其他环境可用 `pnpm dlx @apprun/shiqu`。用户指定入口、版本或项目版本锁优先。先读取该入口的 `help --json`,核对 appVersion、当前输入与可用操作;下面是稳定语义,不是完整输入 Schema。
15
+ - 所有书库操作使用同一绝对路径:显式 `--data-dir` 优先,其次 `SHIQU_DATA_DIR`、已保存位置和默认书库。首次访问自动建库并迁移,无需先 init;不因此更改默认位置或模型配置。书库被其他服务占用时报告占用,不绕过锁。
16
+ - 自主程序使用进程 API 的可执行文件与参数数组,JSON 经 `--input -` 的 stdin 或私有文件传入;不要把标题、路径、原文、提示词拼接成 shell 命令。沿用入口及其工作目录,处理 stdout 与 stderr,设置有限超时。同一书库的 CLI 调用串行执行,避免独占开库冲突。
17
+ - 子命令 stdout 为一个 JSON 信封:成功 `{schemaVersion:1,ok:true,data:...}`,失败 `{schemaVersion:1,ok:false,error:{code,message,details?}}`;日志在 stderr。检查退出码、信封及业务结果。`skill show` 默认是 Markdown,带 `--json` 才是信封。不要把非零退出的 JSON 丢弃,也不要把退出码 0 或 `ok:true` 当作故事完成。
18
+ - 继续使用已保存的提供方与模型;缺配置时查当前 `config` 帮助。密钥仅经已有环境、私有 JSON 文件或 stdin 传入,不进入 argv、对话、报告或日志;不自行切换提供方或计费方式。
19
+
20
+ ## 数据契约
21
+
22
+ 以当前版本实际返回为准;缺失的可选字段表示未提供,不补造。读取结果和操作输入是不同结构,不把整个读取对象直接回写。
23
+
24
+ | 对象 | 字段与含义 |
25
+ | --- | --- |
26
+ | 制作选择 `selection` | `bookId`、`theme`;`artStyle` 使用目录中的 id;`narrativeDetail` 为 `brief\|balanced\|detailed`,表示详略而非固定幅数;`productionPrompt` 为制作要求,`acceptancePrompt` 为验收要求;`length` 默认 `auto`,固定数量范围查当前协议。`storyTitle` 是选中的源故事,`displayTitle` 是独立显示名,改显示名用 rename。 |
27
+ | 本地来源 `source` | `{edition,sectionId,firstUnit,lastUnit,continuations?}`;`edition` 是版本哈希,续段包含章节和起止单元。引用目录或候选返回的真实范围,不能按标题或摘要猜造。 |
28
+ | 故事快照 `session` | `id` 是版本 UUID;`selection`、`state`、`generationControl`、`intake?`、`revision?`、`superseded?`、`faults?`、`story`。`state` 为 `idle\|running\|failed\|completed`,`generationControl` 为 `active\|paused\|stopped`,两者分别检查。`stage` 是展示文案,逻辑判断使用结构化字段。 |
29
+ | 生产进度 | `generationProgress?` 含 `phase`、`frameCount`、`storyboardFrames?`、`reviewStatus?` 等;`productionState?` 为 `running\|queued\|paused`。正文为 null 时仍可能已有部分分镜,结合进度区分“未编排”和“编排中”;未知数量不当作 0。 |
30
+ | 选题 `intake` | `phase` 为 `discovering\|choosing\|preparing\|confirming\|confirmed`;`generation` 是动作的并发前提;`batches[].stories[]` 是已保存候选,`selected` 为 `{batch,index}`,两者从 0 开始;`draft` 是制作设置;`search.state/error` 表示提取状态。 |
31
+ | 故事正文 `story` | 编排未完成时可为 null;已有正文含 `title`、`pages[]`、`sourceText`、`original?`、`sourceReference`、`publication?`。每幅含 `caption`、`prompt`、`image`;空 image 表示缺图,非空地址仍可能不可读。`ready` 不是媒体完整性证明。书架 list 省略 original 并清空 sourceText,要查看原文需 get/source,不能据 list 判定原文丢失。 |
32
+ | 版本 | `stories current` 返回该家族当前快照;`revision` 包含 `parentId/rootId/number`。编辑前读取 `stories revisions` 返回的 `token`,输入为 `{token,changes}`;不根据时间或标题猜当前版本。 |
33
+ | 故障 | `faults[]` 含 `id`、`message`、`page`、`stage`、`state`、`retryAllowed`;恢复输入 `faultId` 取其 id。`run` 返回 `{reason,session}`,reason 表示本次停止原因。 |
34
+
35
+ ## 业务规则
36
+
37
+ - 原文、候选、摘要、分镜、图片和审核历史以保存结果为依据。自由生成的内容用于分析或建议;正式故事的生成、审核和修订通过同一生产实现执行,不另建一条模型流水线或伪造完成记录。
38
+ - 保存意图不调用模型,只有显式 `run` 推进生产,可能产生费用。用户只要求查看、整理、比较、预览或导出时,不运行生产。用户要求的候选、方案确认和生图前检查点必须保留;已有授权足够时继续,缺少影响结果的选择时再询问。
39
+ - 创建、选题动作、编辑、控制和恢复等支持 requestId 的操作,发出前保存 UUID 与原始输入。重放沿用同一 requestId 及原 generation/token/faultId,不能重新取当前值。跨步骤重新读取状态;新动作使用新 UUID。CONFLICT 时停止该项并重新读取,不通过换 requestId 自动重试原动作来掩盖冲突。
40
+ - search/choose/confirm/setup 携带读取时的 `intake.generation`。编辑和重选会创建版本,后续使用操作返回的新 id 或 current 返回的 id;旧版不能继续制作或完整导出。
41
+ - 暂停保持暂停,失败先检查 faults 和已保存依据。恢复必须有用户授权,且针对当前 faultId;审核越过需要明确 override 意图,不能把越过说成审核通过。超时或中断后先读取保存状态,不循环重跑。
42
+ - 批量程序逐项记录目标 id、请求身份、结果和停止原因;遇到需要用户决定、冲突、暂停或故障时停止该项。其他独立项仅在原授权内继续;报告成功、失败、跳过和未处理项,不能把部分完成称为全部完成。
43
+
44
+ ## 可用操作与停止条件
45
+
46
+ 按任务和当前快照组合操作,不机械执行整条流程。具体参数、可编辑字段和输入示例查同版本 help;`api` 仅提供已有持久化/观察白名单,不是任意 HTTP 或推理入口。
47
+
48
+ | 目的 | 生产入口与判断 |
49
+ | --- | --- |
50
+ | 发现数据 | `books/themes/styles` 查目录,`books contents` 查章节;`stories list/get/current` 查书库与当前版本;`source/prompts/review-history/revisions` 查保存依据。 |
51
+ | 新建或选题 | `stories create` 接收 selection 或 `{selection,intake:true}`,默认进入选题;search 保存提取意图,choose 保存候选索引,confirm 保存制作确认,setup 保存其他选题动作。它们不执行生成。 |
52
+ | 执行生产 | `run <id> --until candidates\|prepared\|reviewed\|completed`。candidates 后按用户目标选择或等待;prepared 后检查原文、摘要和方案并确认;reviewed 在审核通过且生图前停止;默认 completed。每次检查返回 reason 和 session,不假定达到了请求边界。 |
53
+ | 解释停止原因 | `choice` 需选题;`frame_count_required` 是旧故事等待幅数确认,按 help 的 frameCount 输入处理;`already_prepared/already_selected` 表示已越过该边界,改用下一步;`paused` 保持暂停,`blocked` 检查故障,`superseded` 查当前版本。不能在同一停止边界空转。 |
54
+ | 修改与控制 | `stories edit` 使用 token/changes 创建新版;rename 仅改显示名。pause/resume/stop 控制制作;retry/override 针对当前故障。reselect/replace/cancel-reselection 按现有草稿协议处理。 |
55
+ | 预览与交付 | preview 输出独立离线 HTML;export 支持 zip/pdf/png/json/md,使用共享序列化、图片编码、水印和元数据规则。自主程序可组织导出清单,不能自己重建作品包。 |
56
+ | 维护 | backup 创建完整私有快照,verify-backup 验证;restore 仅恢复到新的 data-dir,不覆盖原库或自动切换默认位置。diagnostics 输出脱敏诊断;它与私有完整备份用途不同。 |
57
+
58
+ ## 验收与交付
59
+
60
+ - 整理报告:说明使用的书库、筛选范围与当前版本;缺图数量来自实际 pages,尚无分镜时报告“未编排”,部分分镜报告“编排中”,不当作 0 幅完成。分析字段不全时补读 get/current/source,仍缺失则标注未知。
61
+ - 制作完成:重新读取当前版本,核对状态、审核/故障与所有预期画幅。完整 ZIP/PDF/PNG 还必须由现有导出入口验证图片全部可读;不因有 image 地址就宣称可完整导出。预览和 JSON/Markdown 可以交付中间稿,明确未完成部分。
62
+ - 公开 ZIP 默认只含 `故事.json`、`分镜.md`、`署名与转载.txt` 和 `图片/` 下编号 WebP。保留已保存原文、来源、作者与 AI 披露;不包含内部提示词,只有用户明确需要内部记录时使用 `--include-prompts`。
63
+ - 使用用户署名;未提供时说明,只有明确选择不署名才用 `--allow-unsigned`。不伪造作者、不跳过缺图、不改用旧版。产物放在私有书库和应用静态目录之外;普通导出写新路径,不覆盖已有文件。项目若有专用导出约定,沿用其生产导出脚本。
64
+ - 交付实际文件路径和已核实结果;报告未完成项及原因,不承诺尚未验证的生成或媒体完整性。临时程序和私有输入保存在临时目录,清理本次临时输入;保留用户要求的报告和作品。导出、整理或备份不代表授权 Git 提交、推送或发布;不把私有备份存到公开作品目录。
65
+
66
+ ## 版本检查与更新
67
+
68
+ - 每次启用技能,用上述入口执行 `skill check`。它自动发现当前项目祖先目录(截至 Git 根)或全局安装;确认返回的 path 是本次加载的技能。返回 `not_installed` 时,说明未发现安装副本,沿用已读取的正文和当前入口,不为原任务隐式安装技能。若同名副本并存而发现结果不匹配,明确指定 `--scope project|global` 和安装根目录,不更新其他副本。查询缓存 24 小时,用户要求立即检查时加 `--refresh`。离线、旧 CLI 不支持检查或检查失败时简短说明,沿用可用版本继续原任务,不循环重试。
69
+ - 默认只报告新版。仅用户明确要求开启/关闭自动更新时,执行 `skill check --auto-update on|off`(必要时指定范围);不要从一次故事任务推断自动更新授权。
70
+ - 无版本锁且 `autoUpdateReady` 为 true 时,以返回的 updateCwd 为工作目录执行 updateCommand 参数数组;它固定 npm 正式发布版本并带 `--automatic`,更新时会再次检查偏好。本地修改或未知来源副本不自动覆盖。用户明确要求单次更新但未开启自动更新时,也可执行返回的 updateCommand。
71
+ - 更新成功后重新读取返回 path 的技能,用返回 version 对应的 `pnpm dlx @apprun/shiqu@<version> help --json`,后续任务沿用该固定版本。未更新且无用户/项目版本锁、非源码入口时,已管理副本优先用 check 返回的 installedVersion 对应 CLI;自定义副本或源码入口沿用用户选择并核对兼容性。更新失败保留现状,不手工覆盖或删除状态以绕过保护。
72
+ - `skill update --scope project|global` 只安装正在运行的 CLI 包自带技能,不自行寻找最新包。安装记录缺失时,仅与运行包正文完全相同的旧副本可用该版本 `skill install` 建立记录;不同内容需人工比较合并。检查/更新不操作书库或生成配置。