@apprun/shiqu 0.6.0 → 0.6.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.
@@ -130,12 +130,13 @@ __export(schema_exports, {
130
130
  productionDemand: () => productionDemand,
131
131
  productionJobs: () => productionJobs,
132
132
  progress: () => progress,
133
+ readerPublicationDefaults: () => readerPublicationDefaults,
133
134
  readerSessions: () => readerSessions,
134
135
  stories: () => stories
135
136
  });
136
137
  import { sql } from "drizzle-orm";
137
138
  import { sqliteTable, text, integer, real, uniqueIndex, index, primaryKey } from "drizzle-orm/sqlite-core";
138
- var books, stories, jobs, events, progress, libraryDeletions, readerSessions, productionJobs, productionDemand, bookImageReferences;
139
+ var books, stories, jobs, events, progress, libraryDeletions, readerSessions, productionJobs, productionDemand, bookImageReferences, readerPublicationDefaults;
139
140
  var init_schema = __esm({
140
141
  "db/schema.ts"() {
141
142
  "use strict";
@@ -176,6 +177,7 @@ var init_schema = __esm({
176
177
  runtimeId: text("runtime_id").notNull()
177
178
  }, (t) => [uniqueIndex("production_viewer").on(t.sessionId, t.ownerId, t.viewerId), index("production_live_demand").on(t.runtimeId, t.expiresAt)]);
178
179
  bookImageReferences = sqliteTable("book_image_references", { ownerId: text("owner_id").notNull(), bookKey: text("book_key").notNull(), storyId: text("story_id").notNull(), page: integer("page").notNull(), token: text("token").notNull(), assetKey: text("asset_key").notNull(), updatedAt: text("updated_at").notNull() }, (t) => [primaryKey({ columns: [t.ownerId, t.bookKey] })]);
180
+ readerPublicationDefaults = sqliteTable("reader_publication_defaults", { ownerId: text("owner_id").primaryKey(), publication: text("publication").notNull() });
179
181
  }
180
182
  });
181
183
 
@@ -185,7 +187,7 @@ var acceptanceCriteria, acceptancePromptSchema, acceptanceInput, acceptanceDirec
185
187
  var init_reader_acceptance = __esm({
186
188
  "lib/reader-acceptance.ts"() {
187
189
  "use strict";
188
- acceptanceCriteria = (text3 = "") => text3.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
190
+ acceptanceCriteria = (text3 = "") => text3.split(/\r?\n/).map((line2) => line2.trim()).filter(Boolean);
189
191
  acceptancePromptSchema = z4.string().max(1200).refine((text3) => acceptanceCriteria(text3).length <= 20, "\u9A8C\u6536\u8981\u6C42\u6700\u591A20\u6761\uFF0C\u6BCF\u884C\u4E00\u6761\u3002");
190
192
  acceptanceInput = (selection) => selection.acceptancePrompt?.trim() ? { acceptancePrompt: selection.acceptancePrompt, acceptanceCriteria: acceptanceCriteria(selection.acceptancePrompt) } : {};
191
193
  acceptanceDirection = "\u672C\u7BC7\u9A8C\u6536\u8981\u6C42\u7531 acceptanceCriteria \u6309\u884C\u7F16\u53F7\u3002\u5236\u4F5C\u4E0E\u4FEE\u8BA2\u65F6\u9010\u6761\u843D\u5B9E\uFF0C\u4ECD\u987B\u670D\u4ECE\u5DF2\u6838\u539F\u6587\u4E8B\u5B9E\u3001\u9009\u5B9A\u6545\u4E8B\u3001\u753B\u98CE\u3001\u603B\u5E45\u6570\u7B56\u7565\u548C\u8F93\u51FA\u683C\u5F0F\u3002\u5236\u4F5C\u8981\u6C42\u4E0E\u9A8C\u6536\u8981\u6C42\u51B2\u7A81\u3001\u8981\u6C42\u65E0\u539F\u6587\u4F9D\u636E\u7684\u4E8B\u4EF6\u6216\u8D85\u51FA\u6587\u5B57\u5206\u955C\u53EF\u5224\u65AD\u8303\u56F4\u65F6\uFF0C\u5BA1\u6838\u5FC5\u987B\u660E\u786E\u6307\u51FA\uFF0C\u4E0D\u64C5\u81EA\u53D6\u820D\u6216\u4F2A\u9020\u4E8B\u5B9E\u3002";
@@ -705,6 +707,7 @@ function view(row) {
705
707
  const storyboardBatch = doc.storyboard && !p && !doc.draft ? { first: doc.storyboard.pages.length + 1, last: Math.min(doc.storyboard.pages.length + readerStoryboardBatchSize, doc.storyboard.outline.beats.length) } : void 0;
706
708
  const stage = intakePhase === "discovering" ? "\u6B63\u5728\u63D0\u53D6\u5019\u9009\u6545\u4E8B" : intakePhase === "choosing" ? "\u8BF7\u9009\u62E9\u6545\u4E8B" : intakePhase === "confirming" ? "\u8BF7\u786E\u8BA4\u5236\u4F5C\u5185\u5BB9" : waiting ? "\u8BF7\u9009\u62E9\u753B\u5E45\u6570" : step?.kind === "recommend" ? "\u6B63\u5728\u9605\u8BFB\u539F\u6587\uFF0C\u63A8\u8350\u753B\u5E45\u6570" : step?.kind === "plan" ? storyboardBatch ? `\u6B63\u5728\u7F16\u6392\u7B2C ${storyboardBatch.first}\u2013${storyboardBatch.last} \u5E45\u5206\u955C` : selection.length === "auto" || selection.length > readerStoryboardBatchSize ? "\u6B63\u5728\u7F16\u5236\u5206\u955C\u76EE\u5F55" : "\u6B63\u5728\u7F16\u6392\u6545\u4E8B" : step?.kind === "reflection" ? "\u6B63\u5728\u62DF\u5B9A\u6807\u9898\u4E0E\u7ED3\u8BED" : step?.kind === "source" ? "\u6B63\u5728\u4FDD\u5B58\u6545\u4E8B\u539F\u6587" : step?.kind === "review" ? doc.visualReview?.status === "blocked" ? "\u5206\u955C\u5BA1\u7247\u5F85\u4FEE\u8BA2" : doc.visualReview?.repairs ? "\u6B63\u5728\u590D\u6838\u5168\u7BC7\u5206\u955C" : "\u6B63\u5728\u5BA1\u6838\u5168\u7BC7\u5206\u955C" : step?.kind === "repair" ? step.key.endsWith(":0") ? "\u6B63\u5728\u4FEE\u8BA2\u6545\u4E8B\u7ED3\u8BED" : "\u6B63\u5728\u4FEE\u8BA2\u5206\u955C\u753B\u9762" : step?.kind === "audio" ? `\u6B63\u5728\u914D\u4E0A\u7B2C ${step.page + 1} \u5E45\u7684\u58F0\u97F3` : step ? `\u6B63\u5728\u7ED8\u5236\u7B2C ${step.page + 1} \u5E45` : "\u6545\u4E8B\u5DF2\u5B8C\u6574\u5C55\u5F00";
707
709
  const story = p ? { id: row.id, title: selection.displayTitle || p.title, summary: p.summary, bookId: selection.bookId, theme: selection.theme, status: "draft", pages: p.pages.map((v, i) => ({ ...v, image: doc.images[i] || "", audio: doc.audio[i] || "" })), reflection: p.reflection, sourceText: doc.original?.chapterContext?.text || doc.original?.text || "", sourceUrl: doc.original?.url || "", original: doc.original, sourceReference: doc.original?.chapterContext?.title || doc.original?.title || p.sourceReference, adaptationNote: (doc.original ? "AI \u5373\u65F6\u6539\u7F16\uFF0C\u975E\u539F\u6587\u5F15\u8FF0\uFF1B\u539F\u6587\u8282\u9009\u4E0E\u6539\u7F16\u5185\u5BB9\u5206\u5F00\u4FDD\u5B58\u3002" : "AI \u5373\u65F6\u6539\u7F16\uFF0C\u975E\u539F\u6587\u5F15\u8FF0\uFF1B\u539F\u5178\u7EBF\u7D22\u7531\u6A21\u578B\u63D0\u4F9B\uFF0C\u5C1A\u672A\u9010\u6761\u6838\u9A8C\u3002") + p.adaptationNote, checks: { source: !!doc.original, plot: false, visual: false }, revision: doc.revision?.number || 1, createdAt: row.created_at, updatedAt: row.updated_at, generated: true, voiceRequested: selection.voice, artStyleLabel: getArtStyle(selection.artStyle).label, productionPrompt: selection.productionPrompt } : null;
710
+ if (story && doc.publication) story.publication = doc.publication;
708
711
  if (story) story.generationModels = (doc.usage || []).map(({ step: step2, model: model2 }) => ({ step: step2, model: model2 }));
709
712
  const frameCountConfirmed = !!p || !!doc.draft || !!doc.storyboard || !!doc.frameCountConfirmed || !selection.narrativeDetail && typeof selection.length === "number";
710
713
  const generationProgress = { sourceReady: !!doc.original, frameCountConfirmed, frameCount: p?.pages.length ?? doc.draft?.pages.length ?? doc.storyboard?.outline.beats.length ?? (frameCountConfirmed && typeof selection.length === "number" ? selection.length : null), ...doc.storyboard || doc.draft ? { storyboardFrames: doc.draft?.pages.length ?? doc.storyboard.pages.length, storyboardBatch } : {}, ...doc.visualReview ? { reviewStatus: doc.visualReview.status, reviewRound: doc.visualReview.round, repairCompleted: doc.visualReview.repaired.length, repairTotal: doc.visualReview.issues.length, reviewAudits: doc.visualReview.reports.length, repairBatches: doc.visualReview.history?.length, repairPasses: doc.visualReview.repairs, reviewStopReason: doc.visualReview.stopReason, reviewOverridden: !!doc.visualReview.overridden } : {}, phase: intakePhase === "choosing" ? "story-choice" : intakePhase === "confirming" ? "production-confirmation" : waiting ? "frame-count" : step?.kind || "completed", startedAt: null, attempt: row.step_attempts || 0 };
@@ -755,7 +758,7 @@ var init_reader_model = __esm({
755
758
  });
756
759
 
757
760
  // lib/reader-repository.ts
758
- var visible, familyRoot, familyNumber, supersededSql, latestVersionMessage, persistenceError, workerDocument, ReaderRepository;
761
+ var visible, familyRoot, familyNumber, supersededSql, latestVersionMessage, persistenceError, checkpoint, workerDocument, ReaderRepository;
759
762
  var init_reader_repository = __esm({
760
763
  "lib/reader-repository.ts"() {
761
764
  "use strict";
@@ -766,7 +769,8 @@ var init_reader_repository = __esm({
766
769
  supersededSql = (alias = "reader_sessions") => `EXISTS(SELECT 1 FROM reader_sessions newer WHERE newer.user_id=${alias}.user_id AND ${familyRoot("newer")}=${familyRoot(alias)} AND ${familyNumber("newer")}>${familyNumber(alias)})`;
767
770
  latestVersionMessage = "\u8FD9\u662F\u65E7\u7248\u672C\uFF0C\u8BF7\u6253\u5F00\u4E66\u67B6\u4E2D\u7684\u6700\u65B0\u7248\u672C\u7EE7\u7EED\u751F\u6210\u3002";
768
771
  persistenceError = (error) => error?.code === "ERR_SQLITE_ERROR";
769
- workerDocument = "CASE WHEN json_type(document,'$.intake')='object' THEN json_set(?,'$.intake',json(json_set(json_extract(document,'$.intake'),'$.phase',json_extract(?,'$.intake.phase')))) ELSE ? END";
772
+ checkpoint = "json_patch(?,CASE WHEN json_type(document,'$.publication')='object' THEN json_object('publication',json_extract(document,'$.publication')) ELSE '{}' END)";
773
+ workerDocument = `CASE WHEN json_type(document,'$.intake')='object' THEN json_set(${checkpoint},'$.intake',json(json_set(json_extract(document,'$.intake'),'$.phase',json_extract(?,'$.intake.phase')))) ELSE ${checkpoint} END`;
770
774
  ReaderRepository = class {
771
775
  db;
772
776
  constructor(db) {
@@ -787,6 +791,25 @@ var init_reader_repository = __esm({
787
791
  if (!result.meta.changes) throw new ReaderError(409, "\u5206\u955C\u4FDD\u5B58\u540E\u624D\u80FD\u4FEE\u6539\u6545\u4E8B\u540D\u3002");
788
792
  return this.get(id, uid);
789
793
  }
794
+ // Compare only publication metadata; independent image/progress saves must not conflict.
795
+ async savePublication(id, uid, publication, expected) {
796
+ await this.get(id, uid);
797
+ const result = await this.db.prepare(`UPDATE reader_sessions SET document=json_set(document,'$.publication',json(?)),updated_at=?
798
+ WHERE id=? AND user_id=? AND json_type(document,'$.plan')='object' AND ${visible} AND NOT ${supersededSql()}
799
+ AND json_extract(document,'$.publication') IS json_extract(?,'$')`).bind(JSON.stringify(publication), (/* @__PURE__ */ new Date()).toISOString(), id, uid, JSON.stringify(expected)).run();
800
+ if (!result.meta.changes) throw new ReaderError(409, "\u7F72\u540D\u8BF4\u660E\u6216\u6545\u4E8B\u7248\u672C\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u540E\u518D\u4FDD\u5B58\u3002");
801
+ return this.get(id, uid);
802
+ }
803
+ async publicationDefaults(uid) {
804
+ const row = await this.db.prepare("SELECT publication FROM reader_publication_defaults WHERE owner_id=?").bind(uid).first();
805
+ return row ? JSON.parse(row.publication) : null;
806
+ }
807
+ async savePublicationDefaults(uid, publication, expected) {
808
+ const value = JSON.stringify(publication);
809
+ const result = expected === null ? await this.db.prepare("INSERT INTO reader_publication_defaults(owner_id,publication) VALUES(?,?) ON CONFLICT(owner_id) DO NOTHING").bind(uid, value).run() : await this.db.prepare("UPDATE reader_publication_defaults SET publication=? WHERE owner_id=? AND json_extract(publication,'$') IS json_extract(?,'$')").bind(value, uid, JSON.stringify(expected)).run();
810
+ if (!result.meta.changes) throw new ReaderError(409, "\u9ED8\u8BA4\u8BF4\u660E\u5DF2\u7531\u53E6\u4E00\u6B21\u64CD\u4F5C\u66F4\u65B0\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u540E\u518D\u4FDD\u5B58\u3002");
811
+ return publication;
812
+ }
790
813
  // Count every newer submission, including deleted ones, so deletion never promotes history.
791
814
  async list(uid) {
792
815
  return (await this.db.prepare(`SELECT * FROM reader_sessions WHERE user_id = ? AND ${visible}
@@ -853,9 +876,9 @@ var init_reader_repository = __esm({
853
876
  if (!result.meta.changes) throw new ReaderError(409, "\u6545\u4E8B\u6B63\u5728\u66F4\u65B0\uFF0C\u8BF7\u7A0D\u540E\u518D\u4FDD\u5B58\u539F\u6587\u3002");
854
877
  return this.get(row.id, row.user_id);
855
878
  }
856
- async create(id, uid, selection) {
857
- const now = (/* @__PURE__ */ new Date()).toISOString();
858
- await this.db.prepare(`INSERT INTO reader_sessions(id,user_id,selection,document,generation_mode,state,step_attempts,lock_until,page,saved,created_at,updated_at) VALUES(?,?,?,?,'background','idle',0,0,0,0,?,?) ON CONFLICT(id) DO NOTHING`).bind(id, uid, JSON.stringify(selection), JSON.stringify(emptyDocument()), now, now).run();
879
+ async create(id, uid, selection, document = emptyDocument()) {
880
+ const now = (/* @__PURE__ */ new Date()).toISOString(), saved = JSON.stringify(document);
881
+ await this.db.prepare(`INSERT INTO reader_sessions(id,user_id,selection,document,generation_mode,state,step_attempts,lock_until,page,saved,created_at,updated_at) VALUES(?,?,?,COALESCE((SELECT json_set(?,'$.publication',json(publication)) FROM reader_publication_defaults WHERE owner_id=?),?),'background','idle',0,0,0,0,?,?) ON CONFLICT(id) DO NOTHING`).bind(id, uid, JSON.stringify(selection), saved, uid, saved, now, now).run();
859
882
  return this.get(id, uid);
860
883
  }
861
884
  async confirmFrameCount(id, uid, count) {
@@ -1376,8 +1399,8 @@ function sectionNumber(value) {
1376
1399
  return String(value);
1377
1400
  }
1378
1401
  function ruleLines(body2, bullets) {
1379
- return body2.trim().split(/\n/).map((line) => {
1380
- const text3 = line.trim();
1402
+ return body2.trim().split(/\n/).map((line2) => {
1403
+ const text3 = line2.trim();
1381
1404
  if (!text3) return "";
1382
1405
  const sentences = text3.split(/(?<=[。!?])(?=[^”’」』】)"])/u).filter(Boolean);
1383
1406
  if (sentences.length < 2) return text3;
@@ -2867,11 +2890,11 @@ function createCodexRunner(variables, options = {}) {
2867
2890
  child.stdin.on("error", () => fail(new CodexError("PROCESS_EXIT")));
2868
2891
  let text3 = "", image = null, threadId = "", actualModel = "", actualEffort, usage = null;
2869
2892
  lines = createInterface3({ input: child.stdout });
2870
- lines.on("line", (line) => {
2893
+ lines.on("line", (line2) => {
2871
2894
  if (closed) return;
2872
2895
  let message;
2873
2896
  try {
2874
- message = JSON.parse(line);
2897
+ message = JSON.parse(line2);
2875
2898
  } catch {
2876
2899
  fail(new CodexError("INVALID_PROTOCOL"));
2877
2900
  return;
@@ -3161,6 +3184,34 @@ var init_generation = __esm({
3161
3184
  }
3162
3185
  });
3163
3186
 
3187
+ // lib/story-publication.ts
3188
+ import { z as z18 } from "zod";
3189
+ var line, watermarkFontSize, publicationSchema;
3190
+ var init_story_publication = __esm({
3191
+ "lib/story-publication.ts"() {
3192
+ "use strict";
3193
+ line = (max) => z18.string().trim().max(max).refine((value) => !/[\r\n\t]/.test(value), "\u8BF7\u5199\u5728\u4E00\u884C\u5185\u3002");
3194
+ watermarkFontSize = { min: 12, max: 48, default: 24 };
3195
+ publicationSchema = z18.object({
3196
+ author: line(80),
3197
+ workUrl: line(1e3).refine((value) => {
3198
+ if (!value) return true;
3199
+ try {
3200
+ const url = new URL(value);
3201
+ return ["https:", "http:"].includes(url.protocol) && !url.username && !url.password;
3202
+ } catch {
3203
+ return false;
3204
+ }
3205
+ }, "\u4F5C\u54C1\u94FE\u63A5\u8BF7\u4F7F\u7528\u4E0D\u542B\u8D26\u53F7\u5BC6\u7801\u7684 http/https \u5730\u5740\u3002"),
3206
+ contact: line(200),
3207
+ sourceNote: z18.string().trim().max(1200),
3208
+ reuse: z18.string().trim().max(1200),
3209
+ // Optional for historical snapshots: parsing an old CAS expectation must not add new fields.
3210
+ watermarkFontSize: z18.number().int("\u6C34\u5370\u5B57\u53F7\u8BF7\u4F7F\u7528\u6574\u6570\u3002").min(watermarkFontSize.min, "\u6C34\u5370\u5B57\u53F7\u4E0D\u80FD\u5C0F\u4E8E 12 px\u3002").max(watermarkFontSize.max, "\u6C34\u5370\u5B57\u53F7\u4E0D\u80FD\u5927\u4E8E 48 px\u3002").optional()
3211
+ }).strict();
3212
+ }
3213
+ });
3214
+
3164
3215
  // server/book-image-reference.ts
3165
3216
  import { createHash as createHash2, randomUUID as randomUUID4 } from "node:crypto";
3166
3217
  function bookKey(id, custom) {
@@ -3271,7 +3322,7 @@ var init_book_image_reference = __esm({
3271
3322
  });
3272
3323
 
3273
3324
  // lib/reader-revision.ts
3274
- import { z as z18 } from "zod";
3325
+ import { z as z19 } from "zod";
3275
3326
  function revisionImpact(base2, changes) {
3276
3327
  const selection = { ...base2.selection, productionPrompt: changes.productionPrompt ?? base2.selection.productionPrompt, artStyle: changes.artStyle ?? base2.selection.artStyle };
3277
3328
  const requirements = selection.productionPrompt !== base2.selection.productionPrompt;
@@ -3313,7 +3364,7 @@ var init_reader_revision = __esm({
3313
3364
  init_reader_limits();
3314
3365
  init_reader_frame_recommendation();
3315
3366
  init_reader_revision_images();
3316
- revisionChangesSchema = z18.object({ productionPrompt: z18.string().max(1200).optional(), acceptancePrompt: acceptancePromptSchema.optional(), artStyle: z18.enum(artStyleIds).optional(), length: z18.union([z18.number().int().min(readerMinFrames).max(readerMaxFrames), z18.literal("auto")]).optional(), narrativeDetail: narrativeDetailSchema.optional(), plan: planSchema.optional() }).strict();
3367
+ revisionChangesSchema = z19.object({ productionPrompt: z19.string().max(1200).optional(), acceptancePrompt: acceptancePromptSchema.optional(), artStyle: z19.enum(artStyleIds).optional(), length: z19.union([z19.number().int().min(readerMinFrames).max(readerMaxFrames), z19.literal("auto")]).optional(), narrativeDetail: narrativeDetailSchema.optional(), plan: planSchema.optional() }).strict();
3317
3368
  revisionFrameCount = (base2) => base2.plan?.pages.length ?? (typeof base2.selection.length === "number" ? base2.selection.length : base2.frameRecommendation?.count ?? 8);
3318
3369
  preserveRevisionScript = (base2, changes) => !base2.plan && base2.savedFrameCount === revisionFrameCount(base2) && (changes.productionPrompt ?? base2.selection.productionPrompt) === base2.selection.productionPrompt && (changes.narrativeDetail ?? base2.selection.narrativeDetail ?? "balanced") === (base2.selection.narrativeDetail || "balanced") && (changes.length === void 0 || changes.length === base2.selection.length || changes.length === revisionFrameCount(base2));
3319
3370
  }
@@ -3672,7 +3723,7 @@ async function createIntake(repo, id, uid, input, books2) {
3672
3723
  const choice = input.source ? await directBookChoice(books2, input) : null;
3673
3724
  const selection = selectionSchema.parse({ ...input, storyTitle: choice?.entry.title, source: choice?.entry.source, discoverySources: scope?.discoverySources, length: "auto", narrativeDetail: input.narrativeDetail || "balanced" });
3674
3725
  if (selection.bookId === "other" && !selection.customBook?.trim()) throw new ReaderError(400, "\u8BF7\u5148\u586B\u5199\u4E66\u540D\u6216 URL\u3002");
3675
- const creationHash = hash(selection), now = (/* @__PURE__ */ new Date()).toISOString(), doc = { ...emptyDocument(), intake: { phase: "discovering", generation: 0, creationHash, batches: [], briefRequired: true, search: { round: 1, state: "searching", cycleId: id } } };
3726
+ const creationHash = hash(selection), doc = { ...emptyDocument(), intake: { phase: "discovering", generation: 0, creationHash, batches: [], briefRequired: true, search: { round: 1, state: "searching", cycleId: id } } };
3676
3727
  if (choice) {
3677
3728
  doc.original = choice.original;
3678
3729
  doc.intake.phase = "preparing";
@@ -3680,8 +3731,7 @@ async function createIntake(repo, id, uid, input, books2) {
3680
3731
  doc.intake.selected = { batch: 0, index: 0 };
3681
3732
  doc.intake.batches = [{ stories: [{ title: choice.entry.title, description: choice.entry.excerpt, source: choice.entry.source, sourceLabel: choice.original.title.slice(0, 150) }], note: "" }];
3682
3733
  }
3683
- await repo.db.prepare("INSERT INTO reader_sessions(id,user_id,selection,document,generation_mode,generation_control,state,step_attempts,lock_until,page,saved,created_at,updated_at) VALUES(?,?,?,?,'background','active','idle',0,0,0,0,?,?) ON CONFLICT(id) DO NOTHING").bind(id, uid, JSON.stringify(selection), JSON.stringify(doc), now, now).run();
3684
- const row = await repo.get(id, uid);
3734
+ const row = await repo.create(id, uid, selection, doc);
3685
3735
  if (JSON.parse(row.document).intake?.creationHash !== creationHash) throw new ReaderError(409, "\u6B64\u521B\u5EFA\u7F16\u53F7\u5DF2\u7528\u4E8E\u5176\u4ED6\u5185\u5BB9\uFF0C\u8BF7\u91CD\u65B0\u521B\u5EFA\u3002");
3686
3736
  return row;
3687
3737
  }
@@ -3836,7 +3886,7 @@ async function forkRevision(repo, media2, ownerId, parentId, requestId, token2,
3836
3886
  }
3837
3887
  }
3838
3888
  const base2 = { ...original, images };
3839
- const document = { plan, original: doc.original, frameRecommendation: doc.frameRecommendation, frameCountConfirmed: true, visualReview: newReview(), images: [], audio: [], usage: (doc.usage || []).map((item) => ({ ...item, originId: item.originId || parentId })), revision: { parentId, rootId, number: epoch + 1, baseToken: token2, requestHash, base: base2, ...impact.replan ? {} : { previewedRedraw: impact.redraw }, ...lostPages.length ? { lostPages } : {} } };
3889
+ const document = { publication: doc.publication, plan, original: doc.original, frameRecommendation: doc.frameRecommendation, frameCountConfirmed: true, visualReview: newReview(), images: [], audio: [], usage: (doc.usage || []).map((item) => ({ ...item, originId: item.originId || parentId })), revision: { parentId, rootId, number: epoch + 1, baseToken: token2, requestHash, base: base2, ...impact.replan ? {} : { previewedRedraw: impact.redraw }, ...lostPages.length ? { lostPages } : {} } };
3840
3890
  if (plan) document.images = reusableRevisionImages(base2, selection, plan);
3841
3891
  if (impact.preserveScript) {
3842
3892
  document.storyboard = doc.storyboard;
@@ -3845,11 +3895,11 @@ async function forkRevision(repo, media2, ownerId, parentId, requestId, token2,
3845
3895
  const now = (/* @__PURE__ */ new Date()).toISOString();
3846
3896
  const inserted = await repo.db.prepare(`INSERT INTO reader_sessions(id,user_id,selection,document,generation_mode,generation_control,state,step_attempts,lock_until,page,saved,last_read_at,created_at,updated_at)
3847
3897
  SELECT ?,?,?,?,'background',?,'idle',0,0,MIN(p.page,?),p.saved,p.last_read_at,?,? FROM reader_sessions p
3848
- WHERE p.id=? AND p.user_id=? AND json_extract(p.document,'$.plan') IS json_extract(?,'$.plan') AND p.selection=?
3898
+ WHERE p.id=? AND p.user_id=? AND json_extract(p.document,'$.plan') IS json_extract(?,'$.plan') AND p.selection=? AND json_extract(p.document,'$.publication') IS json_extract(?,'$.publication')
3849
3899
  AND (json_extract(p.document,'$.plan') IS NOT NULL OR p.generation_control IN ('paused','stopped') OR ${supersededSql("p")})
3850
3900
  AND NOT EXISTS(SELECT 1 FROM library_deletions WHERE story_id=p.id AND user_id=p.user_id)
3851
3901
  AND (${familyEpoch.replace(" AS epoch", "")})=?
3852
- ON CONFLICT(id) DO NOTHING`).bind(requestId, ownerId, JSON.stringify(selection), JSON.stringify(document), paused ? "paused" : "active", impact.total - 1, now, now, parentId, ownerId, row.document, row.selection, ownerId, rootId, rootId, epoch).run();
3902
+ ON CONFLICT(id) DO NOTHING`).bind(requestId, ownerId, JSON.stringify(selection), JSON.stringify(document), paused ? "paused" : "active", impact.total - 1, now, now, parentId, ownerId, row.document, row.selection, row.document, ownerId, rootId, rootId, epoch).run();
3853
3903
  if (!inserted.meta.changes) {
3854
3904
  const raced = await replay();
3855
3905
  if (raced) return raced;
@@ -3874,7 +3924,7 @@ var init_reader_revisions = __esm({
3874
3924
  });
3875
3925
 
3876
3926
  // lib/reader-api.ts
3877
- import { z as z19 } from "zod";
3927
+ import { z as z20 } from "zod";
3878
3928
  function shelfView(session) {
3879
3929
  if (!session.story) return session;
3880
3930
  const story = { ...session.story };
@@ -3908,31 +3958,39 @@ function createReaderApi(services) {
3908
3958
  }
3909
3959
  if (!services.database) throw new ReaderError(503, "\u4E66\u67B6\u6682\u65F6\u4E0D\u53EF\u7528");
3910
3960
  const repo = new ReaderRepository(services.database);
3961
+ if (path15.join("/") === "publication-defaults") {
3962
+ if (req.method === "GET") return json({ publication: await repo.publicationDefaults(uid) });
3963
+ if (req.method === "POST") {
3964
+ const parsed = z20.object({ publication: publicationSchema, expected: publicationSchema.nullable() }).strict().safeParse(await body(req, 32768));
3965
+ if (!parsed.success) throw new ReaderError(400, parsed.error.issues[0].message);
3966
+ return json({ publication: await repo.savePublicationDefaults(uid, parsed.data.publication, parsed.data.expected) });
3967
+ }
3968
+ }
3911
3969
  if (path15.join("/") === "deletions" && req.method === "GET") return json(await repo.deletions(uid));
3912
3970
  if (path15[0] === "library" && path15.length === 2 && req.method === "DELETE") {
3913
- const id = z19.string().min(1).max(100).parse(path15[1]);
3971
+ const id = z20.string().min(1).max(100).parse(path15[1]);
3914
3972
  return json(await repo.remove(id, uid));
3915
3973
  }
3916
3974
  if (path15[0] === "library" && path15.length === 3 && path15[2] === "restore" && req.method === "POST") {
3917
- const id = z19.string().min(1).max(100).parse(path15[1]);
3975
+ const id = z20.string().min(1).max(100).parse(path15[1]);
3918
3976
  return json(await repo.restore(id, uid));
3919
3977
  }
3920
3978
  if (path15[0] === "library" && path15.length === 3 && path15[2] === "reference") {
3921
- const id = z19.string().min(1).max(100).parse(path15[1]), references = new BookImageReferences(services.database, services.media, uid);
3979
+ const id = z20.string().min(1).max(100).parse(path15[1]), references = new BookImageReferences(services.database, services.media, uid);
3922
3980
  if (req.method === "GET") return json(await references.get(id));
3923
3981
  if (req.method === "POST") {
3924
- const data = z19.object({ page: z19.number().int().min(0).max(readerMaxFrames - 1), starred: z19.boolean(), token: uuid.optional() }).strict().parse(await body(req));
3982
+ const data = z20.object({ page: z20.number().int().min(0).max(readerMaxFrames - 1), starred: z20.boolean(), token: uuid.optional() }).strict().parse(await body(req));
3925
3983
  return json(await references.set(id, data.page, data.starred, data.token));
3926
3984
  }
3927
3985
  }
3928
3986
  if (path15[0] === "library" && path15.length === 3 && path15[2] === "favorite" && req.method === "POST") {
3929
- const id = z19.string().min(1).max(100).parse(path15[1]);
3930
- const b = z19.object({ saved: z19.boolean() }).strict().parse(await body(req));
3987
+ const id = z20.string().min(1).max(100).parse(path15[1]);
3988
+ const b = z20.object({ saved: z20.boolean() }).strict().parse(await body(req));
3931
3989
  return json(await repo.favorite(id, uid, b.saved));
3932
3990
  }
3933
3991
  if (path15[0] === "books" && path15.length === 3 && path15[2] === "contents" && req.method === "GET") {
3934
3992
  if (!services.localBooks) throw new ReaderError(503, "\u672C\u5730\u4E66\u5E93\u6682\u4E0D\u53EF\u7528");
3935
- const query = z19.string().max(200).parse(url.searchParams.get("q") || "");
3993
+ const query = z20.string().max(200).parse(url.searchParams.get("q") || "");
3936
3994
  return json(await bookContents(services.localBooks, selectionSchema.parse({ bookId: path15[1], theme: "\u76EE\u5F55" }), query));
3937
3995
  }
3938
3996
  if (path15.join("/") === "suggestions" && req.method === "POST") {
@@ -3981,7 +4039,7 @@ function createReaderApi(services) {
3981
4039
  if (req.method === "GET") return json(await Promise.all((await repo.list(uid)).filter(readableStory).map(async (row) => shelfView(services.production && row.state !== "completed" ? await services.production.snapshot(uid, row.id) : view(row)))));
3982
4040
  if (req.method === "POST") {
3983
4041
  await requireProvider();
3984
- const b = z19.object({ requestId: uuid, selection: selectionSchema.omit({ displayTitle: true }), intake: z19.boolean().optional() }).parse(await body(req, 32768));
4042
+ const b = z20.object({ requestId: uuid, selection: selectionSchema.omit({ displayTitle: true }), intake: z20.boolean().optional() }).parse(await body(req, 32768));
3985
4043
  if (b.selection.discoverySources && b.intake !== true) throw new ReaderError(400, "\u7AE0\u8282\u63D0\u53D6\u5FC5\u987B\u901A\u8FC7\u9009\u9898\u6D41\u7A0B\u521B\u5EFA\u3002");
3986
4044
  return json(view(b.intake ? await createIntake(repo, b.requestId, uid, b.selection, services.localBooks) : await repo.create(b.requestId, uid, b.selection)), 201);
3987
4045
  }
@@ -3991,10 +4049,21 @@ function createReaderApi(services) {
3991
4049
  if (path15[2] === "prompts" && path15.length === 3 && req.method === "GET") {
3992
4050
  const row = await repo.get(id, uid), doc = JSON.parse(row.document), selection = selectionSchema.parse(JSON.parse(row.selection));
3993
4051
  const preferences = { productionPrompt: selection.productionPrompt, ...acceptanceInput(selection), artDirection: getArtStyle(selection.artStyle).prompt, narrativeDetail: selection.narrativeDetail || "balanced", exactPageCount: selection.length };
3994
- return json({ records: (doc.usage || []).flatMap((item, index2) => item.promptRecord ? [{ step: item.step, model: item.model, originId: item.originId || id, index: index2, record: item.promptRecord }] : []), criteria: acceptanceCriteria(selection.acceptancePrompt), results: doc.visualReview?.reports.at(-1)?.acceptanceResults || [], overridden: !!doc.visualReview?.overridden, previews: { production: promptRecord(draftInstructions(!!doc.original?.localSource) + "\n\n## \u63D0\u7EB2\u9636\u6BB5\n" + outlineDirection + "\n\n## \u5206\u6279\u7EC6\u5316\u9636\u6BB5\n" + batchDirection, preferences), acceptance: promptRecord(reviewSystemPrompt + "\n\n" + acceptanceDirection + "\n\n## \u5B9A\u5411\u4FEE\u8BA2\n" + repairSystemPrompt, preferences) } });
4052
+ const history = {
4053
+ productionPrompt: selection.productionPrompt,
4054
+ acceptancePrompt: selection.acceptancePrompt || "",
4055
+ records: (doc.usage || []).flatMap((item, index2) => item.promptRecord ? [{ step: item.step, model: item.model, originId: item.originId || id, index: index2, record: item.promptRecord }] : []),
4056
+ missingRecords: (doc.usage || []).flatMap((item, index2) => item.promptRecord ? [] : [{ step: item.step, model: item.model, originId: item.originId || id, index: index2 }])
4057
+ };
4058
+ return json({ ...history, criteria: acceptanceCriteria(selection.acceptancePrompt), results: doc.visualReview?.reports.at(-1)?.acceptanceResults || [], overridden: !!doc.visualReview?.overridden, previews: { production: promptRecord(draftInstructions(!!doc.original?.localSource) + "\n\n## \u63D0\u7EB2\u9636\u6BB5\n" + outlineDirection + "\n\n## \u5206\u6279\u7EC6\u5316\u9636\u6BB5\n" + batchDirection, preferences), acceptance: promptRecord(reviewSystemPrompt + "\n\n" + acceptanceDirection + "\n\n## \u5B9A\u5411\u4FEE\u8BA2\n" + repairSystemPrompt, preferences) } });
4059
+ }
4060
+ if (path15[2] === "publication" && path15.length === 3 && req.method === "POST") {
4061
+ const parsed = z20.object({ publication: publicationSchema, expected: publicationSchema.nullable() }).strict().safeParse(await body(req, 32768));
4062
+ if (!parsed.success) throw new ReaderError(400, parsed.error.issues[0].message);
4063
+ return json(view(await repo.savePublication(id, uid, parsed.data.publication, parsed.data.expected)));
3995
4064
  }
3996
4065
  if (path15[2] === "title" && path15.length === 3 && req.method === "POST") {
3997
- const parsed = z19.object({ title: displayTitleSchema }).strict().safeParse(await body(req));
4066
+ const parsed = z20.object({ title: displayTitleSchema }).strict().safeParse(await body(req));
3998
4067
  if (!parsed.success) throw new ReaderError(400, parsed.error.issues[0].message);
3999
4068
  return json(view(await repo.rename(id, uid, parsed.data.title)));
4000
4069
  }
@@ -4010,7 +4079,7 @@ function createReaderApi(services) {
4010
4079
  if (req.method === "POST") {
4011
4080
  await requireProvider();
4012
4081
  if (!services.production) throw new ReaderError(503, "\u5236\u4F5C\u961F\u5217\u6682\u4E0D\u53EF\u7528");
4013
- const b = z19.object({ requestId: uuid, token: z19.string().regex(/^[a-f0-9]{64}$/), changes: revisionChangesSchema, paused: z19.boolean().default(false) }).strict().parse(await body(req, 262144));
4082
+ const b = z20.object({ requestId: uuid, token: z20.string().regex(/^[a-f0-9]{64}$/), changes: revisionChangesSchema, paused: z20.boolean().default(false) }).strict().parse(await body(req, 262144));
4014
4083
  const row = await forkRevision(repo, services.media, uid, id, b.requestId, b.token, b.changes, b.paused);
4015
4084
  const doc = JSON.parse(row.document);
4016
4085
  if (!b.paused && !doc.revision?.handoff) services.production.wake();
@@ -4024,7 +4093,7 @@ function createReaderApi(services) {
4024
4093
  }
4025
4094
  if (path15[2] === "production-control" && path15.length === 3 && req.method === "POST") {
4026
4095
  if (!services.production) throw new ReaderError(503, "\u5236\u4F5C\u961F\u5217\u6682\u4E0D\u53EF\u7528");
4027
- const b = z19.object({ action: z19.enum(["background", "reader", "pause", "stop", "resume"]) }).strict().parse(await body(req));
4096
+ const b = z20.object({ action: z20.enum(["background", "reader", "pause", "stop", "resume"]) }).strict().parse(await body(req));
4028
4097
  if (b.action === "background" || b.action === "resume") await requireProvider();
4029
4098
  return json(await services.production.control(uid, id, b.action));
4030
4099
  }
@@ -4032,11 +4101,11 @@ function createReaderApi(services) {
4032
4101
  if (!services.production) throw new ReaderError(503, "\u5236\u4F5C\u961F\u5217\u6682\u4E0D\u53EF\u7528");
4033
4102
  if (req.method === "POST") {
4034
4103
  await requireProvider();
4035
- const b = z19.object({ viewerId: uuid, page: z19.number().int().min(0).max(readerMaxFrames - 1), retry: z19.boolean().default(false) }).strict().parse(await body(req));
4104
+ const b = z20.object({ viewerId: uuid, page: z20.number().int().min(0).max(readerMaxFrames - 1), retry: z20.boolean().default(false) }).strict().parse(await body(req));
4036
4105
  return json(await services.production.request(uid, id, b.viewerId, b.page, b.retry), 202);
4037
4106
  }
4038
4107
  if (req.method === "DELETE") {
4039
- const b = z19.object({ viewerId: uuid }).strict().parse(await body(req));
4108
+ const b = z20.object({ viewerId: uuid }).strict().parse(await body(req));
4040
4109
  await services.production.leave(uid, id, b.viewerId);
4041
4110
  return json({ ok: true });
4042
4111
  }
@@ -4056,23 +4125,23 @@ function createReaderApi(services) {
4056
4125
  }
4057
4126
  if (path15[2] === "events" && req.method === "POST") {
4058
4127
  const row = await repo.get(id, uid);
4059
- const b = z19.object({ id: z19.string().max(200), visitor: uuid, session: uuid, storyId: uuid, kind: z19.enum(["start", "page", "complete", "next", "save", "audio", "feedback_positive", "feedback_negative"]), page: z19.number().int().min(0).max(readerMaxFrames - 1).nullable(), mode: z19.enum(["manual", "audio"]), cohort: z19.enum(["internal", "pilot"]) }).parse(await body(req));
4128
+ const b = z20.object({ id: z20.string().max(200), visitor: uuid, session: uuid, storyId: uuid, kind: z20.enum(["start", "page", "complete", "next", "save", "audio", "feedback_positive", "feedback_negative"]), page: z20.number().int().min(0).max(readerMaxFrames - 1).nullable(), mode: z20.enum(["manual", "audio"]), cohort: z20.enum(["internal", "pilot"]) }).parse(await body(req));
4060
4129
  const current = view(row);
4061
4130
  if (b.storyId !== id || b.page !== null && !current.story?.pages[b.page]?.image) throw new ReaderError(400, "\u8FD9\u5E45\u753B\u9762\u8FD8\u6CA1\u6709\u5C55\u5F00");
4062
4131
  await services.db.insert(events).values({ ...b, createdAt: (/* @__PURE__ */ new Date()).toISOString() }).onConflictDoNothing();
4063
4132
  return json({ ok: true });
4064
4133
  }
4065
4134
  if (path15[2] === "progress" && req.method === "POST") {
4066
- const b = z19.object({ page: z19.number().int().min(0).max(readerMaxFrames - 1), saved: z19.boolean() }).parse(await body(req));
4135
+ const b = z20.object({ page: z20.number().int().min(0).max(readerMaxFrames - 1), saved: z20.boolean() }).parse(await body(req));
4067
4136
  return json(view(await repo.progress(id, uid, b.page, b.saved)));
4068
4137
  }
4069
4138
  if (path15[2] === "frame-count" && path15.length === 3 && req.method === "POST") {
4070
- const b = z19.object({ count: z19.union([z19.number().int().min(1).max(readerMaxFrames), z19.literal("auto")]) }).strict().parse(await body(req));
4139
+ const b = z20.object({ count: z20.union([z20.number().int().min(1).max(readerMaxFrames), z20.literal("auto")]) }).strict().parse(await body(req));
4071
4140
  return json(view(await repo.confirmFrameCount(id, uid, b.count)));
4072
4141
  }
4073
4142
  if (path15[2] === "advance" && req.method === "POST") {
4074
4143
  await requireProvider();
4075
- const b = z19.object({ throughPage: z19.number().int().min(0).max(readerMaxFrames - 1), retry: z19.boolean().default(false) }).parse(await body(req));
4144
+ const b = z20.object({ throughPage: z20.number().int().min(0).max(readerMaxFrames - 1), retry: z20.boolean().default(false) }).parse(await body(req));
4076
4145
  const result = services.production && await services.production.managed(uid, id) ? await services.production.advance(uid, id, b.throughPage, b.retry) : await advanceStory(repo, withBookReference(provider.generator(), new BookImageReferences(services.database, services.media, uid)), uid, id, b.throughPage, b.retry);
4077
4146
  return json(result, result.state === "running" ? 202 : 200);
4078
4147
  }
@@ -4100,7 +4169,7 @@ function createReaderApi(services) {
4100
4169
  throw new ReaderError(404, "\u627E\u4E0D\u5230\u8FD9\u4E2A\u6545\u4E8B\u5165\u53E3");
4101
4170
  } catch (error) {
4102
4171
  if (error instanceof ReaderError) return json({ error: error.message }, error.status);
4103
- if (error instanceof z19.ZodError) return json({ error: "\u8BF7\u9009\u62E9\u6709\u6548\u7684\u4E66\u7C4D\u3001\u4E3B\u9898\u4E0E\u9875\u6570" }, 400);
4172
+ if (error instanceof z20.ZodError) return json({ error: "\u8BF7\u9009\u62E9\u6709\u6548\u7684\u4E66\u7C4D\u3001\u4E3B\u9898\u4E0E\u9875\u6570" }, 400);
4104
4173
  return json({ error: "\u6545\u4E8B\u6682\u65F6\u65E0\u6CD5\u6253\u5F00\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002" }, 500);
4105
4174
  }
4106
4175
  };
@@ -4109,6 +4178,7 @@ var json, uuid;
4109
4178
  var init_reader_api = __esm({
4110
4179
  "lib/reader-api.ts"() {
4111
4180
  "use strict";
4181
+ init_story_publication();
4112
4182
  init_reader_generation_contract();
4113
4183
  init_reader_storyboard();
4114
4184
  init_reader_review();
@@ -4130,12 +4200,12 @@ var init_reader_api = __esm({
4130
4200
  init_reader_intake2();
4131
4201
  init_reader_revisions();
4132
4202
  json = (data, status = 200) => Response.json(data, { status, headers: { "Cache-Control": "private, no-store" } });
4133
- uuid = z19.string().uuid();
4203
+ uuid = z20.string().uuid();
4134
4204
  }
4135
4205
  });
4136
4206
 
4137
4207
  // lib/validation.ts
4138
- import { z as z20 } from "zod";
4208
+ import { z as z21 } from "zod";
4139
4209
  function publishErrors(s) {
4140
4210
  const e = [];
4141
4211
  if (s.pages.length < 4) e.push("\u81F3\u5C11\u9700\u89814\u9875\u5206\u955C");
@@ -4149,9 +4219,9 @@ var init_validation = __esm({
4149
4219
  "lib/validation.ts"() {
4150
4220
  "use strict";
4151
4221
  init_reader_limits();
4152
- media = z20.string().max(300).refine((s) => s === "" || /^\/(art|api\/media)\/[a-zA-Z0-9._-]+$/.test(s), "\u5A92\u4F53\u5FC5\u987B\u662F\u5DF2\u4E0A\u4F20\u7684\u672C\u7AD9\u6587\u4EF6");
4153
- pageSchema = z20.object({ caption: z20.string().max(400), prompt: z20.string().max(4e3), image: media, audio: media });
4154
- storySchema = z20.object({ title: z20.string().trim().min(1).max(80), summary: z20.string().max(1200), bookId: z20.string().max(100), theme: z20.string().max(60), pages: z20.array(pageSchema).max(readerMaxFrames), reflection: z20.string().max(300), sourceText: z20.string().max(3e4), sourceUrl: z20.string().max(1e3).refine((s) => !s || /^https:\/\//.test(s), "\u6765\u6E90\u94FE\u63A5\u9700\u4E3AHTTPS"), adaptationNote: z20.string().max(2e3), checks: z20.object({ source: z20.boolean(), plot: z20.boolean(), visual: z20.boolean() }), revision: z20.number().int().min(1) });
4222
+ media = z21.string().max(300).refine((s) => s === "" || /^\/(art|api\/media)\/[a-zA-Z0-9._-]+$/.test(s), "\u5A92\u4F53\u5FC5\u987B\u662F\u5DF2\u4E0A\u4F20\u7684\u672C\u7AD9\u6587\u4EF6");
4223
+ pageSchema = z21.object({ caption: z21.string().max(400), prompt: z21.string().max(4e3), image: media, audio: media });
4224
+ storySchema = z21.object({ title: z21.string().trim().min(1).max(80), summary: z21.string().max(1200), bookId: z21.string().max(100), theme: z21.string().max(60), pages: z21.array(pageSchema).max(readerMaxFrames), reflection: z21.string().max(300), sourceText: z21.string().max(3e4), sourceUrl: z21.string().max(1e3).refine((s) => !s || /^https:\/\//.test(s), "\u6765\u6E90\u94FE\u63A5\u9700\u4E3AHTTPS"), adaptationNote: z21.string().max(2e3), checks: z21.object({ source: z21.boolean(), plot: z21.boolean(), visual: z21.boolean() }), revision: z21.number().int().min(1) });
4155
4225
  }
4156
4226
  });
4157
4227
 
@@ -4184,7 +4254,7 @@ var init_studio_prompts = __esm({
4184
4254
 
4185
4255
  // lib/server.ts
4186
4256
  import { eq, and, desc, sql as sql2 } from "drizzle-orm";
4187
- import { z as z21 } from "zod";
4257
+ import { z as z22 } from "zod";
4188
4258
  function createApi(services) {
4189
4259
  const readerApi = createReaderApi(services);
4190
4260
  const vars = () => services.variables;
@@ -4253,7 +4323,7 @@ function createApi(services) {
4253
4323
  }
4254
4324
  async function generate(req) {
4255
4325
  need(vars().SHIQU_AI_PROVIDER !== "readonly", "\u5F53\u524D\u4E3A\u53EA\u8BFB\u6A21\u5F0F\uFF0C\u8BF7\u5148\u914D\u7F6E\u751F\u6210\u670D\u52A1\u3002", 403);
4256
- const b = z21.object({ storyId: z21.string(), kind: z21.enum(["text", "image", "audio"]), page: z21.number().int().min(0).max(15).optional(), requestId: z21.string().uuid() }).parse(await body2(req));
4326
+ const b = z22.object({ storyId: z22.string(), kind: z22.enum(["text", "image", "audio"]), page: z22.number().int().min(0).max(15).optional(), requestId: z22.string().uuid() }).parse(await body2(req));
4257
4327
  const p = provider(b.kind);
4258
4328
  const db = getDb();
4259
4329
  const [existing] = await db.select().from(jobs).where(eq(jobs.id, b.requestId));
@@ -4282,7 +4352,7 @@ function createApi(services) {
4282
4352
  } catch {
4283
4353
  throw new ApiError(502, "\u6A21\u578B\u8FD4\u56DE\u7684JSON\u65E0\u6CD5\u89E3\u6790\uFF0C\u5DF2\u4FDD\u7559\u539F\u7A3F");
4284
4354
  }
4285
- const generated = z21.object({ title: z21.string().min(1).max(80), summary: z21.string().max(1200), reflection: z21.string().max(300), adaptationNote: z21.string().max(2e3), pages: z21.array(z21.object({ caption: z21.string().min(1).max(400), prompt: z21.string().min(1).max(4e3) })).min(4).max(16) }).parse(parsed);
4355
+ const generated = z22.object({ title: z22.string().min(1).max(80), summary: z22.string().max(1200), reflection: z22.string().max(300), adaptationNote: z22.string().max(2e3), pages: z22.array(z22.object({ caption: z22.string().min(1).max(400), prompt: z22.string().min(1).max(4e3) })).min(4).max(16) }).parse(parsed);
4286
4356
  result = { ...generated, pages: generated.pages.map((p2) => ({ ...p2, image: "", audio: "" })), checks: { source: false, plot: false, visual: false } };
4287
4357
  } else if (b.kind === "image") {
4288
4358
  const i = b.page;
@@ -4345,7 +4415,7 @@ function createApi(services) {
4345
4415
  if (route === "admin/status") return json2({ providers: providerStatus(), database: !!services.database, storage: !!services.media, privateLab: true });
4346
4416
  if (route === "books" && method === "GET") return json2(await db.select().from(books));
4347
4417
  if (route === "admin/books" && method === "POST") {
4348
- const b = z21.object({ title: z21.string().min(1).max(100), author: z21.string().max(100), era: z21.string().max(50), sourceUrl: z21.string().max(1e3).refine((s) => !s || /^https:\/\//.test(s), "\u6765\u6E90\u94FE\u63A5\u9700\u4E3AHTTPS"), excerpt: z21.string().max(3e4), note: z21.string().max(1e3) }).parse(await body2(req));
4418
+ const b = z22.object({ title: z22.string().min(1).max(100), author: z22.string().max(100), era: z22.string().max(50), sourceUrl: z22.string().max(1e3).refine((s) => !s || /^https:\/\//.test(s), "\u6765\u6E90\u94FE\u63A5\u9700\u4E3AHTTPS"), excerpt: z22.string().max(3e4), note: z22.string().max(1e3) }).parse(await body2(req));
4349
4419
  const row = { ...b, id: crypto.randomUUID() };
4350
4420
  await db.insert(books).values(row);
4351
4421
  return json2(row);
@@ -4357,7 +4427,7 @@ function createApi(services) {
4357
4427
  }
4358
4428
  if (route === "admin/stories" && method === "GET") return json2((await db.select().from(stories).orderBy(desc(stories.updatedAt))).map(parse));
4359
4429
  if (route === "admin/stories" && method === "POST") {
4360
- const b = z21.object({ title: z21.string().min(1).max(80), bookId: z21.string(), theme: z21.string().max(60), sourceText: z21.string().max(3e4), sourceUrl: z21.string().max(1e3).refine((s2) => !s2 || /^https:\/\//.test(s2), "\u6765\u6E90\u94FE\u63A5\u9700\u4E3AHTTPS") }).parse(await body2(req));
4430
+ const b = z22.object({ title: z22.string().min(1).max(80), bookId: z22.string(), theme: z22.string().max(60), sourceText: z22.string().max(3e4), sourceUrl: z22.string().max(1e3).refine((s2) => !s2 || /^https:\/\//.test(s2), "\u6765\u6E90\u94FE\u63A5\u9700\u4E3AHTTPS") }).parse(await body2(req));
4361
4431
  const s = { ...b, id: crypto.randomUUID(), summary: "", pages: [], reflection: "", adaptationNote: "", status: "draft", checks: { source: false, plot: false, visual: false }, revision: 1, createdAt: now(), updatedAt: now() };
4362
4432
  await db.insert(stories).values(serialize(s));
4363
4433
  return json2(s, 201);
@@ -4382,7 +4452,7 @@ function createApi(services) {
4382
4452
  return json2(next);
4383
4453
  }
4384
4454
  if (method === "POST") {
4385
- const b = z21.object({ action: z21.enum(["review", "publish", "unpublish"]), revision: z21.number() }).parse(await body2(req));
4455
+ const b = z22.object({ action: z22.enum(["review", "publish", "unpublish"]), revision: z22.number() }).parse(await body2(req));
4386
4456
  need(b.revision === s.revision, "\u5185\u5BB9\u5DF2\u53D8\u66F4\uFF0C\u8BF7\u5237\u65B0", 409);
4387
4457
  if (b.action === "publish") {
4388
4458
  const errors = publishErrors(s);
@@ -4422,7 +4492,7 @@ function createApi(services) {
4422
4492
  if (route === "me/progress") {
4423
4493
  const uid = services.ownerId, hidden = new Set((await new ReaderRepository(services.database).deletions(uid)).map((d) => d.storyId));
4424
4494
  if (method === "GET") return json2((await db.select().from(progress).where(eq(progress.userId, uid)).orderBy(desc(progress.updatedAt))).filter((p) => !hidden.has(p.storyId)));
4425
- const b = z21.object({ storyId: z21.string(), page: z21.number().int().min(0).max(readerMaxFrames - 1), saved: z21.boolean() }).parse(await body2(req));
4495
+ const b = z22.object({ storyId: z22.string(), page: z22.number().int().min(0).max(readerMaxFrames - 1), saved: z22.boolean() }).parse(await body2(req));
4426
4496
  need(!hidden.has(b.storyId), "\u8FD9\u7BC7\u6545\u4E8B\u5DF2\u5220\u9664", 404);
4427
4497
  const s = await story(b.storyId);
4428
4498
  need(s.status === "published" && b.page < s.pages.length, "\u65E0\u6548\u8FDB\u5EA6");
@@ -4431,7 +4501,7 @@ function createApi(services) {
4431
4501
  return json2({ ok: true });
4432
4502
  }
4433
4503
  if (route === "events" && method === "POST") {
4434
- const b = z21.object({ id: z21.string().max(200), visitor: z21.string().uuid(), session: z21.string().uuid(), storyId: z21.string().max(100), kind: z21.enum(["start", "page", "complete", "next", "save", "audio", "feedback_positive", "feedback_negative"]), page: z21.number().int().min(0).max(readerMaxFrames - 1).nullable(), mode: z21.enum(["manual", "audio"]), cohort: z21.enum(["internal", "pilot"]) }).parse(await body2(req));
4504
+ const b = z22.object({ id: z22.string().max(200), visitor: z22.string().uuid(), session: z22.string().uuid(), storyId: z22.string().max(100), kind: z22.enum(["start", "page", "complete", "next", "save", "audio", "feedback_positive", "feedback_negative"]), page: z22.number().int().min(0).max(readerMaxFrames - 1).nullable(), mode: z22.enum(["manual", "audio"]), cohort: z22.enum(["internal", "pilot"]) }).parse(await body2(req));
4435
4505
  const s = await story(b.storyId);
4436
4506
  need(s.status === "published", "\u65E0\u6548\u6545\u4E8B");
4437
4507
  if (b.page !== null) need(b.page < s.pages.length, "\u65E0\u6548\u9875\u7801");
@@ -4470,7 +4540,7 @@ function createApi(services) {
4470
4540
  }
4471
4541
  throw new ApiError(404, "\u63A5\u53E3\u4E0D\u5B58\u5728");
4472
4542
  } catch (e) {
4473
- if (e instanceof z21.ZodError) return json2({ error: e.errors.map((x) => x.path.join(".") + ": " + x.message).join("\uFF1B") }, 400);
4543
+ if (e instanceof z22.ZodError) return json2({ error: e.errors.map((x) => x.path.join(".") + ": " + x.message).join("\uFF1B") }, 400);
4474
4544
  if (e instanceof ApiError) return json2({ error: e.message }, e.status);
4475
4545
  console.error(e instanceof Error ? e.message : "server error");
4476
4546
  return json2({ error: "\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\uFF1B\u6570\u636E\u672A\u786E\u8BA4\u4FDD\u5B58\u3002" }, 500);
@@ -4635,9 +4705,9 @@ function chapterNumber(value) {
4635
4705
  }
4636
4706
  function localLines(text3) {
4637
4707
  const result = [];
4638
- for (const line of text3.split("\n").map((p) => p.trim()).filter(Boolean)) {
4708
+ for (const line2 of text3.split("\n").map((p) => p.trim()).filter(Boolean)) {
4639
4709
  let part = "";
4640
- for (const token2 of line.match(/[^。!?!?;;]+[。!?!?;;]?|[。!?!?;;]/gu) || [line]) {
4710
+ for (const token2 of line2.match(/[^。!?!?;;]+[。!?!?;;]?|[。!?!?;;]/gu) || [line2]) {
4641
4711
  if (part.length + token2.length > 2e3 && part) {
4642
4712
  result.push(part);
4643
4713
  part = "";
@@ -4905,28 +4975,28 @@ function parseShidianOriginal(number, html, item) {
4905
4975
  const lines = JSON.parse(paragraph.content).lines;
4906
4976
  if (!Array.isArray(lines) || !lines.length) throw Error("Missing paragraph lines");
4907
4977
  let body2 = "", small = false;
4908
- for (const line of lines) {
4909
- if (![1, 2, 3].includes(line.lineType) || typeof line.content !== "string" || line.marginNoteIdList?.length) throw Error("Unknown source line");
4910
- if (line.lineType === 3 && (line.content || line.charPic)) throw Error("Unknown page marker");
4911
- if (line.lineType === 2 && !small) {
4978
+ for (const line2 of lines) {
4979
+ if (![1, 2, 3].includes(line2.lineType) || typeof line2.content !== "string" || line2.marginNoteIdList?.length) throw Error("Unknown source line");
4980
+ if (line2.lineType === 3 && (line2.content || line2.charPic)) throw Error("Unknown page marker");
4981
+ if (line2.lineType === 2 && !small) {
4912
4982
  body2 += "\u3014\u7248\u5F0F\uFF1A\u5C0F\u5B57\u3015";
4913
4983
  small = true;
4914
- } else if (line.lineType !== 2 && small) {
4984
+ } else if (line2.lineType !== 2 && small) {
4915
4985
  body2 += "\u3014\u5C0F\u5B57\u6B62\u3015";
4916
4986
  small = false;
4917
4987
  }
4918
- if (line.charPic) {
4919
- const pic = line.charPic;
4920
- if (!/^\d+$/.test(pic.charPicId) || typeof pic.unicode !== "string" || pic.unicode.trim() || line.content || typeof pic.url !== "string" || new URL(pic.url).protocol !== "https:") throw Error("Unknown glyph image");
4988
+ if (line2.charPic) {
4989
+ const pic = line2.charPic;
4990
+ if (!/^\d+$/.test(pic.charPicId) || typeof pic.unicode !== "string" || pic.unicode.trim() || line2.content || typeof pic.url !== "string" || new URL(pic.url).protocol !== "https:") throw Error("Unknown glyph image");
4921
4991
  const url2 = new URL(pic.url);
4922
4992
  if (!url2.hostname.endsWith(".byteimg.com") || glyphSources.has(pic.charPicId) && glyphSources.get(pic.charPicId) !== url2.pathname) throw Error("Inconsistent glyph source");
4923
4993
  glyphSources.set(pic.charPicId, url2.pathname);
4924
4994
  glyphIds.push(pic.charPicId);
4925
4995
  body2 += `\u25A1\u3014\u539F\u4EF6\u5B57\u5F62\u56FE\uFF1A${pic.charPicId}\u3015`;
4926
- } else body2 += line.content;
4996
+ } else body2 += line2.content;
4927
4997
  }
4928
4998
  if (small) body2 += "\u3014\u5C0F\u5B57\u6B62\u3015";
4929
- const sourceText2 = lines.map((line) => line.content).join("").replace(/\s/g, "");
4999
+ const sourceText2 = lines.map((line2) => line2.content).join("").replace(/\s/g, "");
4930
5000
  const openingHeading = number === 1 ? index2 > 0 && index2 < 50 : index2 < (number === 27 ? 4 : [4, 28].includes(number) ? 2 : 3);
4931
5001
  const volumeOrAttribution = sourceText2.length < 100 && /^(?:[酉西]?陽[雜雑維]俎(?:[續讀]集)?卷|唐臨淄[段叚]成式柯古撰)/.test(sourceText2);
4932
5002
  if (body2.trim()) blocks.push({ text: body2, narrative: !openingHeading && !volumeOrAttribution && /[。!?!?,;:]/.test(body2) });
@@ -4999,10 +5069,10 @@ function parseKanripoChapter(id, number, raw, url) {
4999
5069
  if (!spec?.repo || number < 1 || number > spec.count || !raw.match(new RegExp(`^#\\+PROPERTY: ID ${spec.repo}\\s*$`, "m")) || !raw.match(new RegExp(`^#\\+PROPERTY: BASEEDITION ${spec.edition}\\s*$`, "m"))) throw Error("Kanripo edition mismatch");
5000
5070
  let title, body2;
5001
5071
  if (spec.edition === "WYG") {
5002
- const lines = raw.replace(/\r\n/g, "\n").split("\n").filter((line) => !line.startsWith("#")).map((line) => line.replace(/<pb:[^>]+>/g, "").replace(/¶/g, ""));
5072
+ const lines = raw.replace(/\r\n/g, "\n").split("\n").filter((line2) => !line2.startsWith("#")).map((line2) => line2.replace(/<pb:[^>]+>/g, "").replace(/¶/g, ""));
5003
5073
  const expected = `${id === "\u641C\u795E\u8BB0" ? "\u641C\u795E\u8A18" : "\u535A\u7269\u5FD7"}[\u5377\u5DFB]${chinese(number)}`;
5004
- const heads = lines.flatMap((line, index2) => new RegExp(`^\\s*${expected}(?:\\s.*)?$`).test(line) ? [index2] : []);
5005
- if (heads.length !== 2 || lines.slice(heads[1] + 1).some((line) => line.trim())) throw Error("Kanripo volume boundary mismatch");
5074
+ const heads = lines.flatMap((line2, index2) => new RegExp(`^\\s*${expected}(?:\\s.*)?$`).test(line2) ? [index2] : []);
5075
+ if (heads.length !== 2 || lines.slice(heads[1] + 1).some((line2) => line2.trim())) throw Error("Kanripo volume boundary mismatch");
5006
5076
  title = `\u5377${chinese(number)}`;
5007
5077
  body2 = lines.slice(heads[0] + 1, heads[1]).join("\n");
5008
5078
  } else {
@@ -5010,7 +5080,7 @@ function parseKanripoChapter(id, number, raw, url) {
5010
5080
  if (heads.length !== 1 || Number(heads[0][1]) !== number) throw Error("Kanripo chapter sequence mismatch");
5011
5081
  title = heads[0][2].trim().replace(/第[一二三四五六七八九十百]+$/, "");
5012
5082
  body2 = raw.slice(heads[0].index + heads[0][0].length).replace(/^#.*$/gm, "").replace(/<pb:[^>]+>/g, "").replace(/¶/g, "");
5013
- body2 = body2.split(/\n\s*\n/).map((part) => part.split("\n").map((line) => line.trim()).join("")).filter(Boolean).join("\n");
5083
+ body2 = body2.split(/\n\s*\n/).map((part) => part.split("\n").map((line2) => line2.trim()).join("")).filter(Boolean).join("\n");
5014
5084
  }
5015
5085
  if (/[<>#¶]/.test(body2) || !body2.trim()) throw Error("Malformed Kanripo narrative");
5016
5086
  const sectionId = `${spec.prefix}-${String(number).padStart(id === "zhuangzi" ? 2 : 3, "0")}`;
@@ -5161,16 +5231,16 @@ var init_local_book_erke = __esm({
5161
5231
  function parseWygVolume(raw, number, url) {
5162
5232
  const juan = raw.match(/^#\+PROPERTY: JUAN [卷巻]([^\r\n]+)$/m);
5163
5233
  if (!/^#\+PROPERTY: ID KR3l0118\s*$/m.test(raw) || !/^#\+PROPERTY: BASEEDITION WYG\s*$/m.test(raw) || !juan || chapterNumber(juan[1]) !== number) throw Error("WYG volume metadata mismatch");
5164
- const lines = raw.replace(/\r\n/g, "\n").split("\n").filter((line) => !line.startsWith("#") && !/^<pb:KR3l0118_WYG_\d+-\d+[ab]>¶$/.test(line)).map((line) => line.replace(/¶$/, ""));
5165
- const titles = lines.flatMap((line, index2) => {
5166
- const match = line.match(/^\u3000[太大]平廣記[卷巻]([〇零一二三四五六七八九十百千]+)(?:\u3000.*)?$/);
5234
+ const lines = raw.replace(/\r\n/g, "\n").split("\n").filter((line2) => !line2.startsWith("#") && !/^<pb:KR3l0118_WYG_\d+-\d+[ab]>¶$/.test(line2)).map((line2) => line2.replace(/¶$/, ""));
5235
+ const titles = lines.flatMap((line2, index2) => {
5236
+ const match = line2.match(/^\u3000[太大]平廣記[卷巻]([〇零一二三四五六七八九十百千]+)(?:\u3000.*)?$/);
5167
5237
  return match ? [{ index: index2, number: chapterNumber(match[1]) }] : [];
5168
5238
  });
5169
5239
  if (titles.length !== 2 || titles.some((title) => title.number !== number)) throw Error("WYG opening/closing volume mismatch");
5170
- const first = lines.findIndex((line) => /^\u3000{4}[^\s]/u.test(line)), last = titles[1].index;
5171
- if (first <= titles[0].index || first >= last || lines.slice(last + 1).some((line) => line.trim())) throw Error("WYG body boundary mismatch");
5240
+ const first = lines.findIndex((line2) => /^\u3000{4}[^\s]/u.test(line2)), last = titles[1].index;
5241
+ if (first <= titles[0].index || first >= last || lines.slice(last + 1).some((line2) => line2.trim())) throw Error("WYG body boundary mismatch");
5172
5242
  const sectionId = `taiping-${String(number).padStart(3, "0")}`;
5173
- const units = lines.slice(first, last).filter((line) => line.trim()).map((line, index2) => ({ id: `${sectionId}-${String(index2 + 1).padStart(3, "0")}`, text: line.trim(), narrative: !/^\u3000{2,}/u.test(line) && [...line.matchAll(new RegExp("\\p{Script=Han}", "gu"))].length >= 12 }));
5243
+ const units = lines.slice(first, last).filter((line2) => line2.trim()).map((line2, index2) => ({ id: `${sectionId}-${String(index2 + 1).padStart(3, "0")}`, text: line2.trim(), narrative: !/^\u3000{2,}/u.test(line2) && [...line2.matchAll(new RegExp("\\p{Script=Han}", "gu"))].length >= 12 }));
5174
5244
  if (!units.some((unit) => unit.narrative) || units.some((unit) => /[¶<>#]/u.test(unit.text))) throw Error("WYG narrative missing or malformed");
5175
5245
  return { id: sectionId, title: juan[0].replace(/^.*JUAN /, "") + "\uFF08\u6587\u6E0A\u9601\u672C\uFF09", units, url };
5176
5246
  }
@@ -5921,7 +5991,7 @@ var init_reader_discovery_v1 = __esm({
5921
5991
 
5922
5992
  // server/story-discovery-cache.ts
5923
5993
  import { createHash as createHash8 } from "node:crypto";
5924
- import { z as z22 } from "zod";
5994
+ import { z as z23 } from "zod";
5925
5995
  function discoveryHash(value) {
5926
5996
  return createHash8("sha256").update(JSON.stringify(value)).digest("hex");
5927
5997
  }
@@ -5950,7 +6020,7 @@ var init_story_discovery_cache = __esm({
5950
6020
  init_reader_model();
5951
6021
  flightsByStore = /* @__PURE__ */ new WeakMap();
5952
6022
  writesByStore = /* @__PURE__ */ new WeakMap();
5953
- envelope = z22.object({ version: z22.literal(1), key: z22.string(), value: z22.unknown() });
6023
+ envelope = z23.object({ version: z23.literal(1), key: z23.string(), value: z23.unknown() });
5954
6024
  StoryDiscoveryCache = class {
5955
6025
  flights = /* @__PURE__ */ new Map();
5956
6026
  updates = /* @__PURE__ */ new Map();
@@ -6109,7 +6179,7 @@ var init_story_discovery_legacy = __esm({
6109
6179
  });
6110
6180
 
6111
6181
  // server/story-discovery-metrics.ts
6112
- import { z as z23 } from "zod";
6182
+ import { z as z24 } from "zod";
6113
6183
  async function settleMeasurements(cache) {
6114
6184
  while (measurementBusy(cache)) await Promise.allSettled([...pendingMeasurements.get(cache.identity)]);
6115
6185
  }
@@ -6166,14 +6236,14 @@ var init_story_discovery_metrics = __esm({
6166
6236
  "server/story-discovery-metrics.ts"() {
6167
6237
  "use strict";
6168
6238
  init_story_discovery_cache();
6169
- schema = z23.object({ started: z23.number(), cold: z23.boolean(), firstStoredMs: z23.number().nullable(), firstMatchMs: z23.number().nullable(), calls: z23.number(), isolationCalls: z23.number(), inputTokens: z23.number(), outputTokens: z23.number(), unknownUsage: z23.number(), status: z23.enum(["searching", "matched", "incomplete", "none", "failed", "cancelled"]), ids: z23.array(z23.string()).max(5e4) });
6239
+ schema = z24.object({ started: z24.number(), cold: z24.boolean(), firstStoredMs: z24.number().nullable(), firstMatchMs: z24.number().nullable(), calls: z24.number(), isolationCalls: z24.number(), inputTokens: z24.number(), outputTokens: z24.number(), unknownUsage: z24.number(), status: z24.enum(["searching", "matched", "incomplete", "none", "failed", "cancelled"]), ids: z24.array(z24.string()).max(5e4) });
6170
6240
  pendingMeasurements = /* @__PURE__ */ new WeakMap();
6171
6241
  measurementBusy = (cache) => !!pendingMeasurements.get(cache.identity)?.size;
6172
6242
  }
6173
6243
  });
6174
6244
 
6175
6245
  // server/story-reading-inputs.ts
6176
- import { z as z24 } from "zod";
6246
+ import { z as z25 } from "zod";
6177
6247
  function render(rows) {
6178
6248
  let previous = "";
6179
6249
  return rows.map(({ section: section2, unit }) => {
@@ -6269,15 +6339,15 @@ var init_story_reading_inputs = __esm({
6269
6339
  init_reader_local_contract();
6270
6340
  init_story_discovery_cache();
6271
6341
  init_story_token_budget();
6272
- blockSchema = z24.object({ ranges: z24.array(localRangeSchema).min(1), tokens: z24.number().int().min(1) });
6273
- manifestSchema = z24.object({ mode: z24.enum(["whole", "chapters"]), blocks: z24.array(blockSchema).min(1) });
6342
+ blockSchema = z25.object({ ranges: z25.array(localRangeSchema).min(1), tokens: z25.number().int().min(1) });
6343
+ manifestSchema = z25.object({ mode: z25.enum(["whole", "chapters"]), blocks: z25.array(blockSchema).min(1) });
6274
6344
  defaultReadingLimits = { whole: wholeInputTokens, chapter: chapterInputTokens };
6275
6345
  }
6276
6346
  });
6277
6347
 
6278
6348
  // server/story-theme-matching.ts
6279
6349
  import { randomUUID as randomUUID5 } from "node:crypto";
6280
- import { z as z25 } from "zod";
6350
+ import { z as z26 } from "zod";
6281
6351
  async function settleMatching(cache) {
6282
6352
  while (matchingBusy(cache)) await Promise.all([...new Set(coordinators.get(cache.identity).flights.values())].map((flight) => flight.promise));
6283
6353
  }
@@ -6318,7 +6388,7 @@ async function matchLibraryStories(provider, cache, namespace, theme, stories2,
6318
6388
  const fits = (input2) => readingTokens(themeMatchPrompt, providerName) + readingTokens(JSON.stringify(themeMatchPayload(theme, input2)), providerName) + readingTokens(JSON.stringify(themeMatchJson), providerName) <= budget;
6319
6389
  const input = (entry) => themeStorySchema.parse({ id: entry.id, title: entry.story.title, text: entry.text });
6320
6390
  for (const legacy of options.legacyNamespaces || []) {
6321
- const old = await cache.read(legacyThemeDecisionKey(legacy, theme, { model: identity.model, reasoningEffort: identity.reasoningEffort }), z25.object({ decisions: z25.record(z25.string(), themeDecisionSchema) }));
6391
+ const old = await cache.read(legacyThemeDecisionKey(legacy, theme, { model: identity.model, reasoningEffort: identity.reasoningEffort }), z26.object({ decisions: z26.record(z26.string(), themeDecisionSchema) }));
6322
6392
  if (old) await cache.update(key, savedSchema, empty, (current) => {
6323
6393
  for (const entry of entries) {
6324
6394
  const decision = old.decisions[legacyThemeStoryId(entry.story)];
@@ -6541,9 +6611,9 @@ var init_story_theme_matching = __esm({
6541
6611
  init_story_discovery_cache();
6542
6612
  init_story_token_budget();
6543
6613
  init_reader_discovery_v1();
6544
- unknownSchema = z25.object({ reason: z25.enum(["failed", "budget"]), cycle: z25.string(), budget: z25.number() });
6545
- savedSchema = z25.object({ decisions: z25.record(z25.string(), themeDecisionSchema), fingerprints: z25.record(z25.string(), z25.string()), unknown: z25.record(z25.string(), unknownSchema) });
6546
- cycleSchema = z25.object({ calls: z25.record(z25.string(), z25.boolean()), retried: z25.record(z25.string(), z25.boolean()), retryAllowed: z25.boolean() });
6614
+ unknownSchema = z26.object({ reason: z26.enum(["failed", "budget"]), cycle: z26.string(), budget: z26.number() });
6615
+ savedSchema = z26.object({ decisions: z26.record(z26.string(), themeDecisionSchema), fingerprints: z26.record(z26.string(), z26.string()), unknown: z26.record(z26.string(), unknownSchema) });
6616
+ cycleSchema = z26.object({ calls: z26.record(z26.string(), z26.boolean()), retried: z26.record(z26.string(), z26.boolean()), retryAllowed: z26.boolean() });
6547
6617
  empty = () => ({ decisions: {}, fingerprints: {}, unknown: {} });
6548
6618
  themeStoryId = (story) => discoveryHash([story.title.normalize("NFKC").trim(), localSourceKey(story.source)]);
6549
6619
  legacyThemeStoryId = (story) => discoveryHash([story.title, localSourceKey(story.source)]);
@@ -6573,7 +6643,7 @@ var init_story_theme_matching = __esm({
6573
6643
  });
6574
6644
 
6575
6645
  // server/local-story-discovery.ts
6576
- import { z as z26 } from "zod";
6646
+ import { z as z27 } from "zod";
6577
6647
  import { randomUUID as randomUUID6 } from "node:crypto";
6578
6648
  function rejectedStory(code, raw) {
6579
6649
  const value = raw, source2 = localSourceSchema.safeParse(value?.source);
@@ -6834,11 +6904,11 @@ var init_local_story_discovery = __esm({
6834
6904
  init_story_token_budget();
6835
6905
  init_local_source_ranges();
6836
6906
  init_story_theme_matching();
6837
- storySchema2 = suggestionBatchSchema.shape.stories.element.omit({ storyBrief: true, matchReason: true }).extend({ source: localSourceSchema, continuedFrom: z26.array(localSourceSchema).min(1).max(16).optional() });
6838
- pendingSchema2 = z26.object({ id: z26.string().min(1).max(64), title: z26.string().min(1).max(100), source: localSourceSchema });
6839
- rejectionSchema = z26.object({ code: z26.enum(["duplicate", "no-new-evidence", "invalid-source", "no-progress", "pending-resolution"]), title: z26.string().max(100).optional(), source: localSourceSchema.optional() });
6840
- stateSchema = z26.object({ stories: z26.array(storySchema2).max(5e4), cursor: z26.number().int().min(0), readThrough: z26.number().int().min(0), pendingEvents: z26.array(pendingSchema2).max(16), exhausted: z26.boolean(), rejected: z26.boolean(), lastRejection: rejectionSchema.optional(), overlapDiagnostics: z26.array(z26.object({ source: localSourceSchema, other: localSourceSchema, ratio: z26.number().min(0).max(1) })).max(32).optional() });
6841
- stateEnvelope = stateSchema.extend({ stories: z26.array(z26.unknown()).max(5e4), pendingEvents: z26.array(z26.unknown()).max(16) });
6907
+ storySchema2 = suggestionBatchSchema.shape.stories.element.omit({ storyBrief: true, matchReason: true }).extend({ source: localSourceSchema, continuedFrom: z27.array(localSourceSchema).min(1).max(16).optional() });
6908
+ pendingSchema2 = z27.object({ id: z27.string().min(1).max(64), title: z27.string().min(1).max(100), source: localSourceSchema });
6909
+ rejectionSchema = z27.object({ code: z27.enum(["duplicate", "no-new-evidence", "invalid-source", "no-progress", "pending-resolution"]), title: z27.string().max(100).optional(), source: localSourceSchema.optional() });
6910
+ stateSchema = z27.object({ stories: z27.array(storySchema2).max(5e4), cursor: z27.number().int().min(0), readThrough: z27.number().int().min(0), pendingEvents: z27.array(pendingSchema2).max(16), exhausted: z27.boolean(), rejected: z27.boolean(), lastRejection: rejectionSchema.optional(), overlapDiagnostics: z27.array(z27.object({ source: localSourceSchema, other: localSourceSchema, ratio: z27.number().min(0).max(1) })).max(32).optional() });
6911
+ stateEnvelope = stateSchema.extend({ stories: z27.array(z27.unknown()).max(5e4), pendingEvents: z27.array(z27.unknown()).max(16) });
6842
6912
  failed = () => new ReaderError(502, "\u672C\u6BB5\u9009\u9898\u672A\u80FD\u5B8C\u6574\u6838\u5BF9\uFF0C\u5DF2\u4FDD\u7559\u8FDB\u5EA6\uFF0C\u8BF7\u91CD\u8BD5\u6216\u6362\u4E2A\u4E3B\u9898\u3002");
6843
6913
  rejectionMessage = (reason) => ({
6844
6914
  duplicate: "\u672C\u6B21\u63D0\u53D6\u53EA\u8FD4\u56DE\u4E86\u5DF2\u4FDD\u5B58\u7684\u6545\u4E8B\uFF0C\u5DF2\u4FDD\u7559\u8FDB\u5EA6\uFF0C\u8BF7\u91CD\u8BD5\u67E5\u627E\u3002",