@mazjq/zhiro 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1154 @@
1
+ import {
2
+ companyKey,
3
+ isAggregate,
4
+ sha,
5
+ upsertCampaign,
6
+ upsertCompany,
7
+ upsertPosition
8
+ } from "./chunk-YYNUPNBH.js";
9
+ import {
10
+ json,
11
+ now,
12
+ tx
13
+ } from "./chunk-5KCN32HQ.js";
14
+ import {
15
+ __name
16
+ } from "./chunk-2SSYR7VW.js";
17
+
18
+ // apps/daemon/src/service/expectations.ts
19
+ var HARD_FIELDS = ["城市", "公司类型"];
20
+ var splitValues = /* @__PURE__ */ __name((v) => String(v ?? "").split(/[、,,;;/|\s]+/).map((s) => s.trim()).filter(Boolean), "splitValues");
21
+ function loadExpectations(db) {
22
+ return db.prepare("select data_json from fact where kind = 'preference'").all().map((r) => {
23
+ try {
24
+ return JSON.parse(r.data_json);
25
+ } catch {
26
+ return {};
27
+ }
28
+ }).filter((d) => d.field && d.value).map((d) => ({ field: String(d.field), values: splitValues(d.value), hard: d.mode === "硬性", note: String(d.note ?? "") }));
29
+ }
30
+ __name(loadExpectations, "loadExpectations");
31
+ var RANK = { pass: 0, unknown: 1, fail: 2 };
32
+ var statusRank = /* @__PURE__ */ __name((s) => s ? RANK[s] : 0, "statusRank");
33
+ var hit = /* @__PURE__ */ __name((text, values) => values.some((v) => text.includes(v) || v.includes(text)), "hit");
34
+ function judge(exps, post, company) {
35
+ const reasons = [];
36
+ if (company.blocked) reasons.push({ field: "公司", status: "fail", evidence: "标了「不投」", hard: true });
37
+ for (const e of exps) {
38
+ if (e.field === "城市") {
39
+ const where = post.cities?.length ? post.cities : company.cities ?? [];
40
+ const status2 = !where.length || where.some((c) => /全国|不限|多地|远程|各地/.test(c)) ? "unknown" : where.some((c) => hit(c, e.values)) ? "pass" : "fail";
41
+ reasons.push({ field: e.field, status: status2, evidence: where.length ? where.slice(0, 6).join("、") : "未写工作地", hard: e.hard });
42
+ } else if (e.field === "公司类型") {
43
+ const t = company.companyType ?? "";
44
+ const status2 = !t ? "unknown" : hit(t, e.values) ? "pass" : "fail";
45
+ reasons.push({ field: e.field, status: status2, evidence: t || "企业类型未知", hard: e.hard });
46
+ }
47
+ }
48
+ const hard = reasons.filter((r) => r.hard);
49
+ const status = hard.some((r) => r.status === "fail") ? "fail" : hard.some((r) => r.status === "unknown") ? "unknown" : "pass";
50
+ const score = reasons.filter((r) => !r.hard).reduce((n, r) => n + (r.status === "pass" ? 1 : r.status === "fail" ? -1 : 0), 0);
51
+ return { status, score, reasons };
52
+ }
53
+ __name(judge, "judge");
54
+
55
+ // apps/daemon/src/service/deadlines.ts
56
+ import { createHash } from "node:crypto";
57
+ import { ulid } from "ulid";
58
+
59
+ // apps/daemon/src/service/errors.ts
60
+ var NotFound = class extends Error {
61
+ static {
62
+ __name(this, "NotFound");
63
+ }
64
+ code = "NOT_FOUND";
65
+ };
66
+ var DomainError = class extends Error {
67
+ constructor(code, message) {
68
+ super(message ?? code);
69
+ this.code = code;
70
+ }
71
+ code;
72
+ static {
73
+ __name(this, "DomainError");
74
+ }
75
+ };
76
+
77
+ // apps/daemon/src/service/post-groups.ts
78
+ var titleKey = /* @__PURE__ */ __name((s) => s.normalize("NFKC").replace(/\s+/g, "").toLowerCase(), "titleKey");
79
+ var held = /* @__PURE__ */ __name((p) => p.status !== "closed" || !!p.app_state || (p.watch_priority ?? 0) > 0, "held");
80
+ function groupPosts(campaignIds, byCampaign) {
81
+ const groups = /* @__PURE__ */ new Map(), keyOf = /* @__PURE__ */ new Map();
82
+ for (const cid of campaignIds) {
83
+ const mine = byCampaign.get(cid) ?? [];
84
+ const seen = /* @__PURE__ */ new Map(), tk = mine.map((p) => titleKey(p.title));
85
+ mine.forEach((p, i) => {
86
+ if (p.status !== "closed") seen.set(tk[i], (seen.get(tk[i]) ?? 0) + 1);
87
+ });
88
+ for (const [i, p] of mine.entries()) {
89
+ const t = tk[i];
90
+ const k = t + ((seen.get(t) ?? 0) > 1 ? "|" + [...json.parse(p.cities, [])].sort().join(",") : "");
91
+ keyOf.set(p.id, k);
92
+ if (!held(p)) continue;
93
+ (groups.get(k) ?? groups.set(k, []).get(k)).push(p);
94
+ }
95
+ }
96
+ return { groups, keyOf };
97
+ }
98
+ __name(groupPosts, "groupPosts");
99
+ var ALL = 5e3;
100
+ function twinsOf(db, companyIds) {
101
+ const some = companyIds && companyIds.length <= ALL;
102
+ return twinsFrom(db.prepare(`select p.id, p.campaign_id, p.title, p.cities, p.status, p.watch_priority, p.closes_at, c.company_id, a.state as app_state
103
+ from position p join campaign c on c.id = p.campaign_id left join application a on a.position_id = p.id and a.state != 'cancelled'
104
+ ${some ? `where c.company_id in (${companyIds.map(() => "?").join(",")})` : ""}`).all(...some ? companyIds : []));
105
+ }
106
+ __name(twinsOf, "twinsOf");
107
+ function twinsFrom(rows) {
108
+ const byCompany = /* @__PURE__ */ new Map();
109
+ for (const r of rows) {
110
+ const cs = byCompany.get(r.company_id) ?? byCompany.set(r.company_id, /* @__PURE__ */ new Map()).get(r.company_id);
111
+ (cs.get(r.campaign_id) ?? cs.set(r.campaign_id, []).get(r.campaign_id)).push(r);
112
+ }
113
+ const out = /* @__PURE__ */ new Map();
114
+ for (const cs of byCompany.values()) {
115
+ const { groups, keyOf } = groupPosts([...cs.keys()], cs);
116
+ for (const [id, k] of keyOf) {
117
+ const g = groups.get(k);
118
+ if (!g || g.length === 1 && g[0].id === id) continue;
119
+ const twins = g.filter((p) => p.id !== id);
120
+ if (twins.length) out.set(id, twins);
121
+ }
122
+ }
123
+ return out;
124
+ }
125
+ __name(twinsFrom, "twinsFrom");
126
+
127
+ // apps/daemon/src/service/deadlines.ts
128
+ var GRADE_RANK = { 本人: -1, 官方: 0, 校方转载: 1, 聚合: 2, 行业线索: 3, 推断: 4 };
129
+ var rank = /* @__PURE__ */ __name((g) => GRADE_RANK[g] ?? 9, "rank");
130
+ var sourceName = /* @__PURE__ */ __name((s) => s.grade === "本人" ? "本人核实" : s.grade === "官方" ? "官网" : isAggregate(s.sourceKind) ? "汇总表" : s.grade === "校方转载" ? "校方转载" : "转载", "sourceName");
131
+ var localDay = /* @__PURE__ */ __name((d = /* @__PURE__ */ new Date()) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`, "localDay");
132
+ function sayDate(day, precision = "day") {
133
+ const [y, m, d] = day.slice(0, 10).split("-").map(Number);
134
+ const year = y === (/* @__PURE__ */ new Date()).getFullYear() ? "" : `${y} 年 `;
135
+ return precision === "month_end" ? `${year}${m} 月底` : precision === "month" ? `${year}${m} 月` : `${year}${m} 月 ${d} 日`;
136
+ }
137
+ __name(sayDate, "sayDate");
138
+ var lastDay = /* @__PURE__ */ __name((f) => (f.dateEnd ?? f.dateStart)?.slice(0, 10) ?? null, "lastDay");
139
+ var hasDate = /* @__PURE__ */ __name((f) => (f.policy === "date" || f.policy === "date_or_until_filled") && !!lastDay(f), "hasDate");
140
+ var src = /* @__PURE__ */ __name((f) => ({ grade: f.grade, sourceKind: f.sourceKind, url: f.url, evidence: f.evidence }), "src");
141
+ function compatible(a, b) {
142
+ if (!hasDate(a) || !hasDate(b)) return hasDate(a) === hasDate(b);
143
+ const range = /* @__PURE__ */ __name((f) => {
144
+ const end = lastDay(f);
145
+ if (f.precision === "day" || f.precision === "datetime") return [end, end];
146
+ return [f.dateStart?.slice(0, 10) ?? (f.precision === "month_end" ? `${end.slice(0, 8)}21` : f.precision === "month" ? `${end.slice(0, 8)}01` : end), end];
147
+ }, "range");
148
+ const [a0, a1] = range(a), [b0, b1] = range(b);
149
+ return a0 <= b1 && b0 <= a1;
150
+ }
151
+ __name(compatible, "compatible");
152
+ function sayFact(f, verified, unspecified) {
153
+ const who = sourceName(f), d = lastDay(f), when = d ? sayDate(d, f.precision) : "";
154
+ if (unspecified) return `${who}各岗位截止不同,本岗位的截止未确认`;
155
+ if (verified) {
156
+ if (f.policy === "date") return f.grade === "本人" ? `本人核实:${when}截止` : `官网截止 ${when}`;
157
+ if (f.policy === "date_or_until_filled") return `官网截止 ${when},招满可能提前结束`;
158
+ if (f.policy === "until_filled") return "招满即止,宜尽早";
159
+ if (f.policy === "rolling") return "滚动招聘,宜尽早";
160
+ if (f.policy === "no_fixed_date") return "官网未设截止,宜尽早";
161
+ return "截止未核实";
162
+ }
163
+ if (hasDate(f)) return `仅来自${who}:${when}截止,待核实`;
164
+ if (f.policy === "until_filled") return `仅来自${who}:招满为止,待核实`;
165
+ if (f.policy === "rolling") return `仅来自${who}:尽快投递,待核实`;
166
+ return "截止未核实";
167
+ }
168
+ __name(sayFact, "sayFact");
169
+ var drive = /* @__PURE__ */ __name((kind) => /实习/.test(kind) ? "intern" : /社招/.test(kind) ? "social" : "campus", "drive");
170
+ var cohorts = /* @__PURE__ */ __name((s) => new Set(s.match(/\d{4}/g) ?? []), "cohorts");
171
+ function sameDrive(a, b) {
172
+ if (a.id === b.id) return true;
173
+ if (drive(a.kind) !== drive(b.kind)) return false;
174
+ const ca = cohorts(a.cohort), cb = cohorts(b.cohort);
175
+ return !ca.size || !cb.size || [...ca].some((y) => cb.has(y));
176
+ }
177
+ __name(sameDrive, "sameDrive");
178
+ function campaignFact(c) {
179
+ const days = new Set((c.deadline_text ?? "").match(/\d{4}-\d{1,2}-\d{1,2}/g) ?? []);
180
+ const base = { subjectType: "campaign", subjectId: c.id, event: "application_close", precision: "day", grade: c.source_grade, sourceKind: c.source_kind, url: c.notice_url || c.apply_url || "", evidence: c.deadline_text ?? "", by: c.source_kind, at: "", explicit: false, id: `c:${c.id}` };
181
+ if (c.closes_at) return { ...base, policy: "date", dateStart: null, dateEnd: c.closes_at, applies: days.size >= 2 ? "campaign_unspecified" : "all_campaign_positions" };
182
+ if (c.deadline_kind === "until_filled" || c.deadline_kind === "rolling") return { ...base, policy: c.deadline_kind, dateStart: null, dateEnd: null, applies: "all_campaign_positions" };
183
+ return null;
184
+ }
185
+ __name(campaignFact, "campaignFact");
186
+ function loadContext(db, companyIds) {
187
+ const where = companyIds?.length ? `where company_id in (${companyIds.map(() => "?").join(",")})` : "";
188
+ const cs = db.prepare(`select id, company_id, kind, cohort, closes_at, deadline_kind, deadline_text, source_grade, source_kind, apply_url, notice_url from campaign ${where}`).all(...companyIds ?? []);
189
+ const campaigns = new Map(cs.map((c) => [c.id, c])), byCompany = /* @__PURE__ */ new Map();
190
+ for (const c of cs) (byCompany.get(c.company_id) ?? byCompany.set(c.company_id, []).get(c.company_id)).push(c);
191
+ const facts = /* @__PURE__ */ new Map();
192
+ for (const r of db.prepare("select * from deadline_fact where superseded_at is null").all()) {
193
+ const f = { id: r.id, subjectType: r.subject_type, subjectId: r.subject_id, event: r.event, policy: r.policy, dateStart: r.date_start, dateEnd: r.date_end, precision: r.precision, applies: r.applies, grade: r.grade, sourceKind: r.source_kind, url: r.url, evidence: r.evidence, by: r.by, at: r.created_at, explicit: true };
194
+ const k = `${f.subjectType}:${f.subjectId}`;
195
+ (facts.get(k) ?? facts.set(k, []).get(k)).push(f);
196
+ }
197
+ const overrides = new Map(db.prepare("select * from deadline_override").all().map((o) => [`${o.subject_type}:${o.subject_id}`, o]));
198
+ return { campaigns, byCompany, facts, overrides, today: localDay() };
199
+ }
200
+ __name(loadContext, "loadContext");
201
+ var UNKNOWN = { policy: "unknown", date: null, dateText: "", status: "unverified", scope: "none", say: "截止未核实", source: null, conflicts: [], expired: false, process: "" };
202
+ var OVERRIDE_SAY = { until_filled: "本人核实:招满即止,宜尽早", rolling: "本人核实:滚动招聘,宜尽早", no_fixed_date: "本人核实:官网未设截止,宜尽早", unknown: "本人核实:官方未写截止" };
203
+ function resolvePost(post, ctx, twins = []) {
204
+ const own = ctx.campaigns.get(post.campaign_id);
205
+ if (!own) return UNKNOWN;
206
+ let siblings = ctx.siblings?.get(own.id);
207
+ if (!siblings) (ctx.siblings ??= /* @__PURE__ */ new Map()).set(own.id, siblings = (ctx.byCompany.get(own.company_id) ?? []).filter((c) => sameDrive(own, c)));
208
+ const ov = ctx.overrides.get(`position:${post.id}`) ?? ctx.overrides.get(`campaign:${own.id}`) ?? siblings.map((c) => ctx.overrides.get(`campaign:${c.id}`)).find(Boolean) ?? (drive(own.kind) === "campus" ? ctx.overrides.get(`company:${own.company_id}`) : void 0);
209
+ if (ov) {
210
+ const date2 = ov.kind === "date" ? ov.date : null;
211
+ return {
212
+ policy: ov.kind,
213
+ date: date2,
214
+ dateText: date2 ? sayDate(date2) : "",
215
+ status: "verified",
216
+ scope: ov.subject_type === "position" ? "post" : "campaign",
217
+ say: date2 ? `本人核实:${sayDate(date2)}截止` : OVERRIDE_SAY[ov.kind] ?? "截止未核实",
218
+ source: { grade: "本人", sourceKind: "override", url: "", evidence: ov.note },
219
+ conflicts: [],
220
+ expired: !!date2 && date2 < ctx.today,
221
+ process: ""
222
+ };
223
+ }
224
+ const all = [...ctx.facts.get(`position:${post.id}`) ?? []];
225
+ if (post.closes_at) all.push({ id: `p:${post.id}`, subjectType: "position", subjectId: post.id, event: "application_close", policy: "date", dateStart: null, dateEnd: post.closes_at, precision: "day", applies: "this_position", grade: own.source_grade, sourceKind: own.source_kind, url: own.apply_url || own.notice_url || "", evidence: "", by: own.source_kind, at: "", explicit: false });
226
+ for (const t of twins) {
227
+ const tc = ctx.campaigns.get(t.campaign_id);
228
+ if (!tc || !siblings.includes(tc)) continue;
229
+ all.push(...ctx.facts.get(`position:${t.id}`) ?? []);
230
+ if (t.closes_at) all.push({ id: `p:${t.id}`, subjectType: "position", subjectId: t.id, event: "application_close", policy: "date", dateStart: null, dateEnd: t.closes_at, precision: "day", applies: "this_position", grade: tc.source_grade, sourceKind: tc.source_kind, url: tc.apply_url || tc.notice_url || "", evidence: "", by: tc.source_kind, at: "", explicit: false });
231
+ }
232
+ for (const c of siblings) {
233
+ all.push(...ctx.facts.get(`campaign:${c.id}`) ?? []);
234
+ const f = campaignFact(c);
235
+ if (f) all.push(f);
236
+ }
237
+ if (drive(own.kind) === "campus") all.push(...ctx.facts.get(`company:${own.company_id}`) ?? []);
238
+ const process = all.filter((f) => f.event !== "application_close" && f.event !== "application_open" && f.event !== "apply_rule" && rank(f.grade) <= 0).sort((a, b) => b.at.localeCompare(a.at)).map((f) => f.evidence).filter(Boolean)[0] ?? "";
239
+ const past = /* @__PURE__ */ __name((f) => Number(hasDate(f) && lastDay(f) < ctx.today), "past");
240
+ const stale2 = /* @__PURE__ */ __name((f) => !!past(f) && (f.subjectType === "campaign" ? f.subjectId !== own.id : f.subjectType === "position" && f.subjectId !== post.id), "stale");
241
+ const closing = all.filter((f) => f.event === "application_close" && f.policy !== "unknown");
242
+ const close = closing.filter((f) => !stale2(f));
243
+ if (!close.length) return process ? { ...UNKNOWN, process } : UNKNOWN;
244
+ const direct = /* @__PURE__ */ __name((f) => f.applies === "this_position" ? 0 : f.applies === "all_campaign_positions" ? 1 : 2, "direct");
245
+ const listedBy = /* @__PURE__ */ new Map([[post.id, own.id]]);
246
+ for (const t of twins) if (siblings.some((c) => c.id === t.campaign_id)) listedBy.set(t.id, t.campaign_id);
247
+ const listings = new Set(listedBy.values());
248
+ const listing = /* @__PURE__ */ __name((f) => f.subjectType === "position" ? listedBy.get(f.subjectId) : f.subjectType === "campaign" && listings.has(f.subjectId) ? f.subjectId : void 0, "listing");
249
+ close.sort((a, b) => rank(a.grade) - rank(b.grade) || direct(a) - direct(b) || Number(!!listing(b)) - Number(!!listing(a)) || past(a) - past(b) || Number(b.explicit) - Number(a.explicit) || (lastDay(a) ?? "9").localeCompare(lastDay(b) ?? "9"));
250
+ const best = close[0], verified = rank(best.grade) <= 0, unspecified = best.applies === "campaign_unspecified";
251
+ const date = unspecified ? null : lastDay(best);
252
+ const since = localDay(new Date(Date.now() - 60 * 864e5));
253
+ const recent = closing.filter((f) => stale2(f) && rank(f.grade) < rank(best.grade) && lastDay(f) >= since);
254
+ const conflicts = unspecified ? [] : [...close.slice(1), ...recent].filter((f) => hasDate(f) && f.applies !== "campaign_unspecified" && !compatible(best, f)).filter((f) => !(f.grade === best.grade && (past(f) || f.sourceKind === best.sourceKind && !f.explicit && !(listing(f) && listing(best) && listing(f) !== listing(best))))).filter((f, i, arr) => arr.findIndex((x) => lastDay(x) === lastDay(f) && sourceName(x) === sourceName(f)) === i).map((f) => ({ say: `${sourceName(f)} ${sayDate(lastDay(f), f.precision)}`, date: lastDay(f), source: src(f) }));
255
+ let say = sayFact(best, verified && !unspecified, unspecified);
256
+ if (conflicts.length) say += `;来源有分歧:${conflicts.map((c) => c.say).join("、")}`;
257
+ return {
258
+ policy: unspecified ? "unknown" : best.policy,
259
+ date,
260
+ dateText: date ? sayDate(date, best.precision) : "",
261
+ status: verified && !unspecified ? "verified" : "unverified",
262
+ scope: unspecified ? "unspecified" : best.applies === "this_position" ? "post" : "campaign",
263
+ say,
264
+ source: src(best),
265
+ conflicts,
266
+ expired: !!date && date < ctx.today,
267
+ process
268
+ };
269
+ }
270
+ __name(resolvePost, "resolvePost");
271
+ function applyRules(ctx, companyId) {
272
+ const subjects = [`company:${companyId}`, ...(ctx.byCompany.get(companyId) ?? []).map((c) => `campaign:${c.id}`)];
273
+ return subjects.flatMap((k) => ctx.facts.get(k) ?? []).filter((f) => f.event === "apply_rule").sort((a, b) => b.at.localeCompare(a.at)).filter((f, i, arr) => arr.findIndex((x) => x.evidence === f.evidence) === i).slice(0, 3).map((f) => ({ text: f.evidence, url: f.url }));
274
+ }
275
+ __name(applyRules, "applyRules");
276
+ function summarize(rs) {
277
+ const open = rs.filter((r) => !r.expired);
278
+ const days = open.map((r) => r.date).filter(Boolean).sort();
279
+ const isAsap = /* @__PURE__ */ __name((r) => !r.date && r.status === "verified" && (r.policy === "until_filled" || r.policy === "rolling" || r.policy === "no_fixed_date"), "isAsap");
280
+ const asap = open.filter(isAsap).length, unverified = open.filter((r) => !r.date && !isAsap(r)).length;
281
+ if (!days.length && open.length && open.every((r) => r.say === open[0].say)) return { text: open[0].say, earliest: null, latest: null, asap, unverified };
282
+ const short = /* @__PURE__ */ __name((d) => d.slice(0, 4) === String((/* @__PURE__ */ new Date()).getFullYear()) ? d.slice(5) : d, "short");
283
+ const range = !days.length ? "" : days[0] === days[days.length - 1] ? `截止 ${short(days[0])}` : `岗位截止 ${short(days[0])} ~ ${short(days[days.length - 1])}`;
284
+ const rest = [asap ? `${asap} 个招满即止` : "", unverified ? `${unverified} 个待核实` : ""].filter(Boolean).join("、");
285
+ return { text: range && rest ? `${range};另有 ${rest}` : range || (rest ? rest : rs.length ? "已截止" : ""), earliest: days[0] ?? null, latest: days[days.length - 1] ?? null, asap, unverified };
286
+ }
287
+ __name(summarize, "summarize");
288
+ function resolveMany(db, posts, twins) {
289
+ const out = /* @__PURE__ */ new Map();
290
+ if (!posts.length) return out;
291
+ const companies = /* @__PURE__ */ new Set();
292
+ for (const p of posts) companies.add(p.company_id);
293
+ const ids = [...companies];
294
+ const ctx = loadContext(db, ids.length <= 5e3 ? ids : void 0);
295
+ twins ??= twinsOf(db, ids);
296
+ const own = /* @__PURE__ */ new Set();
297
+ for (const k of [...ctx.facts.keys(), ...ctx.overrides.keys()]) if (k.startsWith("position:")) own.add(k.slice(9));
298
+ const shared = /* @__PURE__ */ new Map();
299
+ for (const p of posts) {
300
+ const tw = twins.get(p.id);
301
+ if (own.size && (own.has(p.id) || tw?.some((t) => own.has(t.id)))) {
302
+ out.set(p.id, resolvePost(p, ctx, tw ?? []));
303
+ continue;
304
+ }
305
+ let k = p.closes_at ?? "";
306
+ if (tw) for (const t of tw) k += `|${t.campaign_id}|${t.closes_at ?? ""}`;
307
+ const byDay = shared.get(p.campaign_id) ?? shared.set(p.campaign_id, /* @__PURE__ */ new Map()).get(p.campaign_id);
308
+ let r = byDay.get(k);
309
+ if (!r) byDay.set(k, r = resolvePost(p, ctx, tw ?? []));
310
+ out.set(p.id, r);
311
+ }
312
+ return out;
313
+ }
314
+ __name(resolveMany, "resolveMany");
315
+ function expiredMany(db, posts, opts = {}) {
316
+ const k = tracked(db), day = localDay();
317
+ const kept = k.day === day && !k.all ? k.expired : null;
318
+ if (!kept) {
319
+ const r = resolveMany(db, posts, opts.twins?.()), out2 = /* @__PURE__ */ new Map();
320
+ for (const p of posts) out2.set(p.id, !!r.get(p.id)?.expired);
321
+ if (opts.every) {
322
+ k.day = day;
323
+ k.all = false;
324
+ k.dirty.clear();
325
+ k.expired = out2;
326
+ }
327
+ return out2;
328
+ }
329
+ const stale2 = posts.filter((p) => !kept.has(p.id) || k.dirty.size > 0 && k.dirty.has(p.company_id));
330
+ if (!stale2.length) return kept;
331
+ const fresh = resolveMany(db, stale2);
332
+ if (opts.every) {
333
+ for (const p of stale2) kept.set(p.id, !!fresh.get(p.id)?.expired);
334
+ k.dirty.clear();
335
+ return kept;
336
+ }
337
+ const out = /* @__PURE__ */ new Map();
338
+ for (const p of posts) out.set(p.id, fresh.has(p.id) ? !!fresh.get(p.id).expired : kept.get(p.id));
339
+ return out;
340
+ }
341
+ __name(expiredMany, "expiredMany");
342
+ var keptOpen = /* @__PURE__ */ new WeakMap();
343
+ var companyOfPost = /* @__PURE__ */ __name((p) => `(select c.company_id from position p join campaign c on c.id = p.campaign_id where p.id = ${p})`, "companyOfPost");
344
+ var companyOfSubject = /* @__PURE__ */ __name((r) => `(case ${r}.subject_type when 'company' then ${r}.subject_id when 'campaign' then (select company_id from campaign where id = ${r}.subject_id) when 'position' then ${companyOfPost(`${r}.subject_id`)} end)`, "companyOfSubject");
345
+ var changed = /* @__PURE__ */ __name((cols) => cols.map((c) => `old.${c} is not new.${c}`).join(" or "), "changed");
346
+ var POST_COLS = ["title", "cities", "status", "watch_priority", "closes_at", "campaign_id"];
347
+ var CAMPAIGN_COLS = ["company_id", "kind", "cohort", "closes_at", "deadline_kind", "deadline_text", "source_grade", "source_kind", "apply_url", "notice_url"];
348
+ var ofCampaign = /* @__PURE__ */ __name((r) => `(select company_id from campaign where id = ${r}.campaign_id)`, "ofCampaign");
349
+ var DEADLINE_INPUTS = [
350
+ ["p_new", "after insert on main.position", [ofCampaign("new")]],
351
+ ["p_gone", "after delete on main.position", [ofCampaign("old")]],
352
+ ["p_upd", `after update of ${POST_COLS.join(", ")} on main.position when ${changed(POST_COLS)}`, [ofCampaign("old"), ofCampaign("new")]],
353
+ ["c_new", "after insert on main.campaign", ["new.company_id"]],
354
+ ["c_gone", "after delete on main.campaign", ["old.company_id"]],
355
+ ["c_upd", `after update of ${CAMPAIGN_COLS.join(", ")} on main.campaign when ${changed(CAMPAIGN_COLS)}`, ["old.company_id", "new.company_id"]],
356
+ ["a_new", "after insert on main.application", [companyOfPost("new.position_id")]],
357
+ ["a_gone", "after delete on main.application", [companyOfPost("old.position_id")]],
358
+ ["a_upd", "after update on main.application", [companyOfPost("old.position_id"), companyOfPost("new.position_id")]],
359
+ ...["deadline_fact", "deadline_override"].flatMap((t, i) => [
360
+ [`${i}_new`, `after insert on main.${t}`, [companyOfSubject("new")]],
361
+ [`${i}_gone`, `after delete on main.${t}`, [companyOfSubject("old")]],
362
+ [`${i}_upd`, `after update on main.${t}`, [companyOfSubject("old"), companyOfSubject("new")]]
363
+ ])
364
+ ];
365
+ function tracked(db) {
366
+ let k = keptOpen.get(db);
367
+ if (!k) {
368
+ const kept = k = { day: "", all: true, dirty: /* @__PURE__ */ new Set(), expired: null };
369
+ keptOpen.set(db, kept);
370
+ db.function("jobos_deadline_inputs_changed", (company) => {
371
+ if (typeof company === "string") kept.dirty.add(company);
372
+ else kept.all = true;
373
+ return null;
374
+ });
375
+ }
376
+ const have = db.prepare("select count(*) n from sqlite_temp_master where type = 'trigger' and name like 'jobos_dl_%'").get().n;
377
+ if (have < DEADLINE_INPUTS.length) {
378
+ for (const [name, when, companies] of DEADLINE_INPUTS) db.exec(`create temp trigger if not exists jobos_dl_${name} ${when} begin ${companies.map((c) => `select jobos_deadline_inputs_changed(${c});`).join(" ")} end`);
379
+ k.all = true;
380
+ }
381
+ return k;
382
+ }
383
+ __name(tracked, "tracked");
384
+ function resolveFor(db, positionIds) {
385
+ const posts = [];
386
+ for (let i = 0; i < positionIds.length; i += 5e3) {
387
+ const part = positionIds.slice(i, i + 5e3);
388
+ posts.push(...db.prepare(`select p.id, p.campaign_id, p.closes_at, c.company_id from position p join campaign c on c.id = p.campaign_id where p.id in (${part.map(() => "?").join(",")})`).all(...part));
389
+ }
390
+ return resolveMany(db, posts);
391
+ }
392
+ __name(resolveFor, "resolveFor");
393
+ var campaignSaying = /* @__PURE__ */ __name((ctx, campaignId) => resolvePost({ id: "", campaign_id: campaignId, closes_at: null }, ctx), "campaignSaying");
394
+ function openCampaigns(db, companyIds) {
395
+ const ctx = loadContext(db, companyIds);
396
+ return new Set([...ctx.campaigns.keys()].filter((id) => !campaignSaying(ctx, id).expired));
397
+ }
398
+ __name(openCampaigns, "openCampaigns");
399
+ function campaignClosesAt(db, campaignId, sourceDate) {
400
+ const o = db.prepare("select kind, date from deadline_override where subject_type = 'campaign' and subject_id = ?").get(campaignId);
401
+ return o ? o.kind === "date" ? o.date : null : sourceDate;
402
+ }
403
+ __name(campaignClosesAt, "campaignClosesAt");
404
+ function recordFact(db, p) {
405
+ const subjectType = p.positionId ? "position" : p.campaignId ? "campaign" : p.companyId ? "company" : null;
406
+ const subjectId = p.positionId ?? p.campaignId ?? p.companyId;
407
+ if (!subjectType || !subjectId) throw new DomainError("MISSING_FIELD", "要说明这是哪个岗位、招聘项目或公司的截止");
408
+ if (!db.prepare(`select 1 from ${subjectType} where id = ?`).get(subjectId)) throw new NotFound(`${subjectType} ${subjectId}`);
409
+ if (!p.evidence?.trim()) throw new DomainError("MISSING_FIELD", "需要原文证据句(来源页面上怎么写的)");
410
+ if (!(p.grade in GRADE_RANK) || p.grade === "本人") throw new DomainError("BAD_GRADE", "grade 为 官方 / 校方转载 / 聚合;本人的判断用 campaign.deadline");
411
+ const event = p.event ?? "application_close";
412
+ const dated = p.policy === "date" || p.policy === "date_or_until_filled";
413
+ if (event === "application_close" && dated && !/^\d{4}-\d{2}-\d{2}/.test(p.date ?? "")) throw new DomainError("BAD_DATE", "具体截止要给 date(YYYY-MM-DD;「12 月底」写 date=12-31、precision=month_end)");
414
+ const applies = p.applies ?? (subjectType === "position" ? "this_position" : "all_campaign_positions");
415
+ const hash = createHash("sha256").update([event, p.policy, p.dateStart ?? "", p.date ?? "", p.precision ?? "day", applies, p.grade, p.url ?? "", p.evidence.trim()].join("|")).digest("hex").slice(0, 32);
416
+ const dup = db.prepare("select id from deadline_fact where subject_type = ? and subject_id = ? and content_hash = ?").get(subjectType, subjectId, hash);
417
+ if (dup) {
418
+ db.prepare("update deadline_fact set superseded_at = null where id = ?").run(dup.id);
419
+ return { id: dup.id, created: false };
420
+ }
421
+ if (p.url) db.prepare("update deadline_fact set superseded_at = ? where subject_type = ? and subject_id = ? and event = ? and url = ? and superseded_at is null").run(now(), subjectType, subjectId, event, p.url);
422
+ const id = ulid();
423
+ db.prepare("insert into deadline_fact(id, subject_type, subject_id, event, policy, date_start, date_end, precision, applies, grade, source_kind, url, evidence, content_hash, by, created_at) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)").run(id, subjectType, subjectId, event, p.policy, p.dateStart ?? null, dated || event !== "application_close" ? p.date ?? null : null, p.precision ?? "day", applies, p.grade, p.sourceKind ?? "", p.url ?? "", p.evidence.trim().slice(0, 600), hash, p.by ?? "", now());
424
+ return { id, created: true };
425
+ }
426
+ __name(recordFact, "recordFact");
427
+ function setOverride(db, p) {
428
+ if (p.kind === "clear") {
429
+ db.prepare("delete from deadline_override where subject_type = ? and subject_id = ?").run(p.subjectType, p.subjectId);
430
+ return;
431
+ }
432
+ if (p.kind === "date" && !/^\d{4}-\d{2}-\d{2}$/.test(p.date ?? "")) throw new DomainError("BAD_DATE", "日期要写成 YYYY-MM-DD");
433
+ db.prepare("insert or replace into deadline_override(subject_type, subject_id, kind, date, note, by, at) values (?,?,?,?,?,?,?)").run(p.subjectType, p.subjectId, p.kind, p.kind === "date" ? p.date : null, p.note ?? "", p.by ?? "", now());
434
+ }
435
+ __name(setOverride, "setOverride");
436
+
437
+ // apps/daemon/src/service/pool.ts
438
+ var GRADE_RANK2 = { 本人: -1, 官方: 0, 校方转载: 1, 聚合: 2, 行业线索: 3, 推断: 4 };
439
+ var rank2 = /* @__PURE__ */ __name((g) => GRADE_RANK2[g] ?? 9, "rank");
440
+ var KIND = { date: "date", date_or_until_filled: "date", until_filled: "until_filled", rolling: "rolling" };
441
+ var unrelated = /* @__PURE__ */ new WeakMap();
442
+ var poolUnrelatedWrite = /* @__PURE__ */ __name((db) => {
443
+ unrelated.set(db, (unrelated.get(db) ?? 0) + 1);
444
+ }, "poolUnrelatedWrite");
445
+ var stamp = /* @__PURE__ */ __name((db, days) => `${db.prepare("select total_changes() as n").get().n - (unrelated.get(db) ?? 0)}|${days}|${localDay()}`, "stamp");
446
+ var userWrote = false;
447
+ var poolDirty = /* @__PURE__ */ __name(() => {
448
+ userWrote = true;
449
+ }, "poolDirty");
450
+ var rebuilding = null;
451
+ var cache = null;
452
+ var MATCHED_BY = ["公司名", "别名", "公司名开头", "公司名包含", "岗位名"];
453
+ function matchRank(q, name, aliases) {
454
+ const n = name.toLowerCase(), a = aliases.map((x) => x.toLowerCase());
455
+ return n === q ? 0 : a.includes(q) ? 1 : n.startsWith(q) ? 2 : n.includes(q) || a.some((x) => x.includes(q)) ? 3 : 4;
456
+ }
457
+ __name(matchRank, "matchRank");
458
+ function build(db, newWithinDays) {
459
+ const campaigns = db.prepare("select c.*, co.name as company_name, co.type as company_type, co.aliases as company_aliases from campaign c join company co on co.id = c.company_id").all();
460
+ const positions = db.prepare("select p.id, p.campaign_id, p.title, p.cities, p.status, p.watch_priority, p.created_at, p.closes_at, a.state as app_state, f.score as fit_score, f.verdict as fit_verdict from position p left join application a on a.position_id = p.id and a.state != 'cancelled' left join position_fit f on f.position_id = p.id").all();
461
+ const recs = db.prepare("select * from recommendation order by score desc").all();
462
+ const newSince = new Date(Date.now() - newWithinDays * 864e5).toISOString();
463
+ const dctx = loadContext(db), resolved = /* @__PURE__ */ new Map();
464
+ const sayings = /* @__PURE__ */ new Map();
465
+ const say = /* @__PURE__ */ __name((c) => sayings.get(c.id) ?? sayings.set(c.id, campaignSaying(dctx, c.id)).get(c.id), "say");
466
+ const over = /* @__PURE__ */ __name((c) => say(c).expired, "over");
467
+ const word = /* @__PURE__ */ __name((c) => dctx.overrides.get(`campaign:${c.id}`), "word");
468
+ const byCompany = /* @__PURE__ */ new Map();
469
+ for (const c of campaigns) (byCompany.get(c.company_id) ?? byCompany.set(c.company_id, []).get(c.company_id)).push(c);
470
+ const posByCampaign = /* @__PURE__ */ new Map();
471
+ for (const p of positions) (posByCampaign.get(p.campaign_id) ?? posByCampaign.set(p.campaign_id, []).get(p.campaign_id)).push(p);
472
+ const recsByCompany = /* @__PURE__ */ new Map();
473
+ for (const r of recs) (recsByCompany.get(r.company_id) ?? recsByCompany.set(r.company_id, []).get(r.company_id)).push(r);
474
+ const titleOf = new Map(positions.map((p) => [p.id, p.title]));
475
+ const rows = [];
476
+ const counts = /* @__PURE__ */ new Map();
477
+ const gradeOf = new Map(campaigns.map((c) => [c.id, c.source_grade]));
478
+ for (const [companyId, cs] of byCompany) {
479
+ cs.sort((a, b) => Number(over(a)) - Number(over(b)) || rank2(a.source_grade) - rank2(b.source_grade) || String(b.last_verified_at ?? "").localeCompare(String(a.last_verified_at ?? "")));
480
+ const live = cs.filter((c) => !over(c));
481
+ const pick = /* @__PURE__ */ __name((f) => cs.find((c) => c[f])?.[f] ?? "", "pick");
482
+ const sg = /* @__PURE__ */ __name((c) => rank2(say(c).source?.grade ?? c.source_grade), "sg");
483
+ const dated = live.filter((c) => say(c).date).sort((a, b) => sg(a) - sg(b) || String(say(a).date).localeCompare(String(say(b).date)));
484
+ const dl = dated[0] ?? live.find((c) => say(c).policy !== "unknown") ?? live[0] ?? cs[0];
485
+ const conflicts = say(dl).conflicts.map((x) => ({
486
+ field: "closesAt",
487
+ value: x.date ?? "",
488
+ grade: x.source.grade,
489
+ sourceKind: x.source.sourceKind,
490
+ batch: cs.find((c) => c.source_kind === x.source.sourceKind && c.closes_at === x.date)?.batch ?? ""
491
+ }));
492
+ const merged = /* @__PURE__ */ new Map();
493
+ for (const [k, ps] of groupPosts(cs.map((c) => c.id), posByCampaign).groups) {
494
+ const p = ps[0];
495
+ merged.set(k, {
496
+ id: p.id,
497
+ title: p.title,
498
+ cities: [...new Set(ps.flatMap((x) => json.parse(x.cities, [])))],
499
+ rows: ps,
500
+ status: p.status,
501
+ grades: [...new Set(ps.map((x) => gradeOf.get(x.campaign_id)))],
502
+ applicationState: ps.find((x) => x.app_state)?.app_state ?? null,
503
+ watched: ps.some((x) => x.watch_priority > 0),
504
+ isNew: ps.every((x) => x.created_at >= newSince),
505
+ match: null,
506
+ fit: p.fit_score == null ? null : { score: p.fit_score, verdict: p.fit_verdict }
507
+ });
508
+ }
509
+ for (const m of merged.values()) {
510
+ const [p, ...twins] = m.rows;
511
+ const own = twins.length > 0 || dctx.facts.has(`position:${p.id}`) || dctx.overrides.has(`position:${p.id}`), rk = `${p.campaign_id}|${p.closes_at ?? ""}`;
512
+ m.deadline = own ? resolvePost(p, dctx, twins) : resolved.get(rk) ?? resolved.set(rk, resolvePost(p, dctx)).get(rk);
513
+ m.closesAt = m.deadline.date;
514
+ delete m.rows;
515
+ }
516
+ const mine = recsByCompany.get(companyId) ?? [];
517
+ const tags = [];
518
+ for (const r of mine) if (!tags.some((x) => x.tag === r.tag)) tags.push({ tag: r.tag, mode: r.mode, score: r.score, reason: r.reason });
519
+ for (const t of tags) counts.set(t.tag, (counts.get(t.tag) ?? 0) + 1);
520
+ const pos = [...merged.values()];
521
+ const sum = summarize(pos.map((p) => p.deadline));
522
+ const matchedKeys = new Set(mine.filter((r) => r.position_id).map((r) => titleKey(titleOf.get(r.position_id) ?? "")));
523
+ const groups = [["准备申请的岗位", pos.filter((p) => p.applicationState === "draft")], ["关注的岗位", pos.filter((p) => p.watched && !p.applicationState)], ["匹配的岗位", pos.filter((p) => matchedKeys.has(titleKey(p.title)))], ["岗位", pos]];
524
+ const focus = groups.find(([, g]) => g.some((p) => !p.deadline.expired)) ?? groups[3];
525
+ const first = focus[1].filter((p) => p.deadline.date && !p.deadline.expired).sort((a, b) => a.deadline.date.localeCompare(b.deadline.date))[0];
526
+ const allExpired = pos.length > 0 && pos.every((p) => p.deadline.expired);
527
+ rows.push({
528
+ keys: [...merged.keys()],
529
+ recs: mine.filter((r) => r.position_id).map((r) => ({ tag: r.tag, score: r.score, reason: r.reason, key: titleKey(titleOf.get(r.position_id) ?? "") })),
530
+ name: cs[0].company_name,
531
+ aliases: json.parse(cs[0].company_aliases, []),
532
+ search: (cs[0].company_name + "\n" + json.parse(cs[0].company_aliases, []).join("\n") + "\n" + pos.map((p) => p.title).join("\n")).toLowerCase(),
533
+ item: {
534
+ companyId,
535
+ companyName: cs[0].company_name,
536
+ companyType: cs[0].company_type ?? "",
537
+ // with posts: by their resolved deadlines; a company that is only a sheet row with no posts goes by its batch
538
+ closesAt: pos.length ? first?.deadline.date ?? null : say(dl).date,
539
+ deadlineKind: pos.length ? first ? "date" : sum.asap ? "until_filled" : "unknown" : KIND[say(dl).policy] ?? "unknown",
540
+ deadlineText: pos.length ? sum.text : say(dl).source?.grade === "本人" && say(dl).source.evidence || dl.deadline_text,
541
+ deadline: { summary: sum.text, earliest: sum.earliest, latest: sum.latest, asap: sum.asap, unverified: sum.unverified, why: first ? `${focus[0]}「${first.title}」${first.deadline.say}` : "", process: pos.find((p) => p.deadline.process)?.deadline.process ?? "", rules: applyRules(dctx, companyId) },
542
+ expired: pos.length ? allExpired : live.length === 0,
543
+ applyUrl: pick("apply_url"),
544
+ noticeUrl: pick("notice_url"),
545
+ degreeReq: pick("degree_req"),
546
+ majorReq: pick("major_req"),
547
+ cities: Array.from(new Set(cs.flatMap((c) => json.parse(c.cities, [])))).slice(0, 20),
548
+ grade: (live[0] ?? cs[0]).source_grade,
549
+ sources: cs.map((c) => ({
550
+ campaignId: c.id,
551
+ sourceKind: c.source_kind,
552
+ grade: c.source_grade,
553
+ batch: c.batch,
554
+ cohort: c.cohort,
555
+ closesAt: word(c) ? word(c).kind === "date" ? word(c).date : null : c.closes_at ?? null,
556
+ deadlineText: word(c)?.note || c.deadline_text,
557
+ positions: (posByCampaign.get(c.id) ?? []).length,
558
+ lastVerifiedAt: c.last_verified_at ?? null
559
+ })),
560
+ conflicts,
561
+ positions: pos,
562
+ newPositions: pos.filter((p) => p.isNew).length,
563
+ tags,
564
+ applied: pos.some((p) => p.applicationState && p.applicationState !== "draft")
565
+ }
566
+ });
567
+ }
568
+ return { rows, tagCounts: [...counts].map(([tag, count]) => ({ tag, count })) };
569
+ }
570
+ __name(build, "build");
571
+ function ensure(db, days) {
572
+ if (!cache || cache.db !== db || cache.days !== days) rebuild(db, days);
573
+ else if (stale(cache, db, days)) {
574
+ if (userWrote) rebuild(db, days);
575
+ else if (rebuilding !== db) {
576
+ rebuilding = db;
577
+ setTimeout(() => {
578
+ try {
579
+ if (cache?.db === db && cache.days === days && stale(cache, db, days)) rebuild(db, days);
580
+ } catch {
581
+ } finally {
582
+ if (rebuilding === db) rebuilding = null;
583
+ }
584
+ }, 0);
585
+ }
586
+ }
587
+ if (!cache) throw new Error("unreachable");
588
+ return cache;
589
+ }
590
+ __name(ensure, "ensure");
591
+ function rebuild(db, days) {
592
+ userWrote = false;
593
+ cache = { db, key: stamp(db, days), days, at: Date.now(), ...build(db, days) };
594
+ }
595
+ __name(rebuild, "rebuild");
596
+ var stale = /* @__PURE__ */ __name((c, db, days) => c.key !== stamp(db, days) || Date.now() - c.at > 6e5, "stale");
597
+ function refreshPool(db) {
598
+ if (!cache || cache.db !== db || cache.key === stamp(db, cache.days)) return false;
599
+ rebuild(db, cache.days);
600
+ return true;
601
+ }
602
+ __name(refreshPool, "refreshPool");
603
+ function deadlineBoard(db, days = 14, verifyLimit = 5) {
604
+ const today = localDay(), until = localDay(new Date(Date.now() + days * 864e5)), soon = localDay(new Date(Date.now() + 14 * 864e5));
605
+ const left = /* @__PURE__ */ __name((d) => Math.round((Date.parse(d) - Date.parse(today)) / 864e5), "left");
606
+ const deadlines = [], risky = [];
607
+ for (const r of ensure(db, 3).rows) {
608
+ const c = r.item, score = c.tags.reduce((m, t) => Math.max(m, t.score), 0);
609
+ const held2 = c.positions.filter((p) => p.watched && !p.applicationState || p.applicationState === "draft" || p.applicationState === "submitted");
610
+ const byHand = c.tags.some((t) => t.mode === "manual" && t.tag !== "不投");
611
+ if (!held2.length && !byHand) continue;
612
+ const posts = held2.length ? held2 : [c.positions.find((p) => !p.deadline.expired && p.deadline.date === c.closesAt) ?? c.positions.find((p) => !p.deadline.expired)].filter(Boolean);
613
+ for (const p of posts) {
614
+ const d = p.deadline, applied = p.applicationState === "submitted";
615
+ const it = {
616
+ companyId: c.companyId,
617
+ companyName: c.companyName,
618
+ positionId: held2.length ? p.id : null,
619
+ title: held2.length ? p.title : "",
620
+ cities: p.cities ?? [],
621
+ closesAt: d.date,
622
+ daysLeft: d.date ? left(d.date) : null,
623
+ applied,
624
+ say: d.say,
625
+ verified: d.status === "verified",
626
+ conflict: d.conflicts.length > 0,
627
+ grade: d.source?.grade ?? c.grade,
628
+ url: d.source?.url || c.noticeUrl || c.applyUrl,
629
+ why: ""
630
+ };
631
+ if (d.date && d.date >= today && d.date <= until) deadlines.push(it);
632
+ if (applied || d.expired) continue;
633
+ const risk = d.status === "unverified" && d.date && d.date <= soon ? 0 : d.conflicts.length ? 1 : d.status === "unverified" ? 2 : -1;
634
+ if (risk >= 0) risky.push({ it: { ...it, why: ["来源写的截止就在两周内,但还没在官网核实", "不同来源写的截止不一样", "还没有可靠的截止信息"][risk] }, risk, held: Number(held2.length > 0), score });
635
+ }
636
+ }
637
+ deadlines.sort((a, b) => String(a.closesAt).localeCompare(String(b.closesAt)));
638
+ risky.sort((a, b) => a.risk - b.risk || b.held - a.held || b.score - a.score || String(a.it.closesAt ?? "9").localeCompare(String(b.it.closesAt ?? "9")));
639
+ return { deadlines, verify: risky.slice(0, verifyLimit).map((x) => x.it) };
640
+ }
641
+ __name(deadlineBoard, "deadlineBoard");
642
+ function listPool(db, q = {}) {
643
+ const cache2 = ensure(db, q.newWithinDays ?? 3);
644
+ const words = (q.query ?? "").trim().toLowerCase().split(/\s+/).filter(Boolean);
645
+ const scoreOf = /* @__PURE__ */ __name((r) => q.tag ? r.item.tags.find((t) => t.tag === q.tag)?.score ?? 0 : r.item.tags.reduce((m, t) => Math.max(m, t.score), 0), "scoreOf");
646
+ const hits = cache2.rows.filter((r) => (!q.companyId || r.item.companyId === q.companyId) && words.every((w) => r.search.includes(w)) && (!q.companyType || String(r.item.companyType ?? "").includes(q.companyType)) && (!q.recommendedOnly || r.item.tags.length > 0) && (!q.tag || r.item.tags.some((t) => t.tag === q.tag))).map((r) => ({ r, score: scoreOf(r) }));
647
+ const tagFor = q.tag ?? "画像匹配";
648
+ const floor = q.minTier === "优先考虑" ? 200 : q.minTier === "可迁移" ? 100 : 0;
649
+ const postFilter = q.city || q.matchedOnly || floor ? (r, p) => {
650
+ const where = p.cities?.length ? p.cities : r.item.cities;
651
+ if (q.city && !where.some((c) => c.includes(q.city))) return false;
652
+ if (!q.matchedOnly && !floor) return true;
653
+ const rec = r.recs.find((x) => x.tag === tagFor && x.key === titleKey(p.title));
654
+ return !!rec && rec.score >= floor;
655
+ } : null;
656
+ if (postFilter) {
657
+ for (let i = hits.length - 1; i >= 0; i--) if (!hits[i].r.item.positions.some((p) => postFilter(hits[i].r, p))) hits.splice(i, 1);
658
+ }
659
+ const exps = loadExpectations(db);
660
+ const verdicts = /* @__PURE__ */ new Map();
661
+ if (exps.length || q.hideExpectationFail) for (let i = hits.length - 1; i >= 0; i--) {
662
+ const r = hits[i].r, co = { companyType: r.item.companyType, cities: r.item.cities, blocked: r.item.tags.some((t) => t.tag === "不投") };
663
+ const byPost = /* @__PURE__ */ new Map();
664
+ for (const p of r.item.positions) if (!postFilter || postFilter(r, p)) byPost.set(p.id, judge(exps, p, co));
665
+ const vs = byPost.size ? [...byPost.values()] : [judge(exps, {}, co)];
666
+ const status = vs.reduce((s, v2) => statusRank(v2.status) < statusRank(s) ? v2.status : s, "fail"), score = Math.max(...vs.map((v2) => v2.score));
667
+ if (q.hideExpectationFail && status === "fail") hits.splice(i, 1);
668
+ else verdicts.set(r, { status, score, byPost });
669
+ }
670
+ const sort = q.sort ?? (q.recommendedOnly || q.tag ? "match" : "deadline");
671
+ const byDeadline = /* @__PURE__ */ __name((a, b) => Number(a.r.item.closesAt == null) - Number(b.r.item.closesAt == null) || String(a.r.item.closesAt).localeCompare(String(b.r.item.closesAt)), "byDeadline");
672
+ const top = /* @__PURE__ */ __name((h) => Number(h.r.item.tags.some((t) => t.tag === "重点")), "top");
673
+ const v = /* @__PURE__ */ __name((h) => verdicts.get(h.r), "v");
674
+ const asked = words.join(" "), rel = /* @__PURE__ */ new Map();
675
+ if (asked) for (const h of hits) rel.set(h.r, matchRank(asked, h.r.name, h.r.aliases));
676
+ hits.sort((a, b) => (rel.get(a.r) ?? 0) - (rel.get(b.r) ?? 0) || Number(a.r.item.expired) - Number(b.r.item.expired) || (sort === "match" ? top(b) - top(a) || statusRank(v(a)?.status) - statusRank(v(b)?.status) || (v(b)?.score ?? 0) - (v(a)?.score ?? 0) || b.score - a.score : sort === "new" ? b.r.item.newPositions - a.r.item.newPositions : 0) || byDeadline(a, b));
677
+ const from = q.offset ?? 0;
678
+ const items = hits.slice(from, from + (q.limit ?? 200)).map(({ r, score }) => {
679
+ const vr = verdicts.get(r);
680
+ const positions = r.item.positions.filter((p) => (!postFilter || postFilter(r, p)) && !(q.hideExpectationFail && vr?.byPost.get(p.id)?.status === "fail")).map((p) => ({ ...p, ...vr?.byPost.has(p.id) ? { expectation: vr.byPost.get(p.id) } : {} }));
681
+ const at = /* @__PURE__ */ new Map();
682
+ for (const p of positions) (at.get(titleKey(p.title)) ?? at.set(titleKey(p.title), []).get(titleKey(p.title))).push(p);
683
+ for (const rec of q.tag ? r.recs.filter((x) => x.tag === q.tag) : r.recs) for (const p of at.get(rec.key) ?? []) if (!p.match) p.match = { score: rec.score, reason: rec.reason };
684
+ positions.sort((a, b) => (b.fit?.score ?? -1) - (a.fit?.score ?? -1) || (b.match?.score ?? 0) - (a.match?.score ?? 0) || statusRank(a.expectation?.status) - statusRank(b.expectation?.status));
685
+ return { ...r.item, score, positions, ...vr ? { expectation: { status: vr.status, score: vr.score } } : {}, ...rel.has(r) ? { matchedBy: MATCHED_BY[rel.get(r)] } : {} };
686
+ });
687
+ return { items, total: hits.length, tagCounts: cache2.tagCounts };
688
+ }
689
+ __name(listPool, "listPool");
690
+ function poolWatched(db, positionId, watched) {
691
+ if (!cache || cache.db !== db) return;
692
+ const p = cache.rows.flatMap((r) => r.item.positions).find((x) => x.id === positionId);
693
+ if (!p) {
694
+ userWrote = true;
695
+ return;
696
+ }
697
+ p.watched = watched;
698
+ const now2 = stamp(db, cache.days), [n, ...rest] = cache.key.split("|");
699
+ if (`${Number(n) + 1}|${rest.join("|")}` === now2) cache.key = now2;
700
+ }
701
+ __name(poolWatched, "poolWatched");
702
+ function searchCompanies(db, query, limit = 10) {
703
+ const like = `%${query.trim()}%`;
704
+ const q = query.trim().toLowerCase();
705
+ const rows = db.prepare(`select co.id, co.name, co.aliases, co.type, co.industry, (select count(*) from position p join campaign c on c.id = p.campaign_id where c.company_id = co.id) as n
706
+ from company co where co.name like ? or co.aliases like ? order by length(co.name) limit 200`).all(like, like);
707
+ return { items: rows.map((r) => ({ r, rank: matchRank(q, r.name, json.parse(r.aliases, [])) })).sort((a, b) => a.rank - b.rank || b.r.n - a.r.n).slice(0, limit).map(({ r, rank: rank3 }) => ({ companyId: r.id, companyName: r.name, type: r.type, industry: r.industry, positions: r.n, matchedBy: MATCHED_BY[rank3] })) };
708
+ }
709
+ __name(searchCompanies, "searchCompanies");
710
+ function poolFacets(db) {
711
+ const top = /* @__PURE__ */ __name((values, n) => {
712
+ const c = /* @__PURE__ */ new Map();
713
+ for (const v of values) if (v) c.set(v, (c.get(v) ?? 0) + 1);
714
+ return [...c].sort((a, b) => b[1] - a[1]).slice(0, n).map(([v]) => v);
715
+ }, "top");
716
+ const cities = db.prepare("select cities from campaign").all().flatMap((r) => json.parse(r.cities, []));
717
+ const cos = db.prepare("select type, industry from company").all();
718
+ const split = /* @__PURE__ */ __name((s) => (s ?? "").split(/[、,,\s]+/), "split");
719
+ return { cities: top(cities, 80), industries: top(cos.flatMap((r) => split(r.industry)), 40), companyTypes: top(cos.flatMap((r) => split(r.type)), 20) };
720
+ }
721
+ __name(poolFacets, "poolFacets");
722
+ function scanHistory(db, names, limit = 30) {
723
+ const rows = db.prepare("select * from scan_run order by started_at desc limit ?").all(limit);
724
+ return { items: rows.map((r) => ({ runId: r.id, source: r.source, configId: r.config_id, name: names.get(r.config_id) ?? "", at: r.started_at, found: r.found, newPositions: r.new_positions, complete: !!r.complete, error: r.error })) };
725
+ }
726
+ __name(scanHistory, "scanHistory");
727
+
728
+ // apps/daemon/src/service/domain.ts
729
+ import { ulid as ulid2 } from "ulid";
730
+ var ALLOWED = {
731
+ draft: ["submitted", "cancelled"],
732
+ cancelled: ["draft"],
733
+ submitted: ["in_process", "closed"],
734
+ in_process: ["in_process", "closed"],
735
+ closed: ["in_process"]
736
+ };
737
+ function campaignRow(r) {
738
+ return {
739
+ id: r.id,
740
+ companyId: r.company_id,
741
+ companyName: r.company_name ?? "",
742
+ kind: r.kind,
743
+ cohort: r.cohort,
744
+ batch: r.batch,
745
+ cities: json.parse(r.cities, []),
746
+ degreeReq: r.degree_req,
747
+ majorReq: r.major_req,
748
+ opensAt: r.opens_at,
749
+ closesAt: r.closes_at,
750
+ deadlineKind: r.deadline_kind,
751
+ deadlineText: r.deadline_text,
752
+ applyUrl: r.apply_url,
753
+ noticeUrl: r.notice_url,
754
+ sourceGrade: r.source_grade,
755
+ sourceKind: r.source_kind,
756
+ hasWrittenTest: r.has_written_test,
757
+ notes: r.notes,
758
+ lastVerifiedAt: r.last_verified_at,
759
+ createdAt: r.created_at,
760
+ updatedAt: r.updated_at
761
+ };
762
+ }
763
+ __name(campaignRow, "campaignRow");
764
+ function positionRow(r) {
765
+ return {
766
+ id: r.id,
767
+ campaignId: r.campaign_id,
768
+ title: r.title,
769
+ jdText: r.jd_text,
770
+ cities: json.parse(r.cities, []),
771
+ degreeReq: r.degree_req,
772
+ majorReq: r.major_req || r.campaign_major_req || "",
773
+ status: r.status,
774
+ directionTags: json.parse(r.direction_tags, []),
775
+ lastSeenAt: r.last_seen_at,
776
+ watchPriority: r.watch_priority,
777
+ companyName: r.company_name ?? "",
778
+ closesAt: r.closes_at ?? null,
779
+ deadlineKind: r.deadline_kind ?? "unknown",
780
+ applyUrl: r.apply_url ?? "",
781
+ noticeUrl: r.notice_url ?? "",
782
+ applicationState: r.app_state ?? null
783
+ };
784
+ }
785
+ __name(positionRow, "positionRow");
786
+ function applicationRow(r) {
787
+ return {
788
+ id: r.id,
789
+ positionId: r.position_id,
790
+ campaignId: r.campaign_id,
791
+ state: r.state,
792
+ closeReason: r.close_reason,
793
+ channel: r.channel,
794
+ externalAppId: r.external_app_id,
795
+ referralCode: r.referral_code,
796
+ resumeLabel: r.resume_label ?? "",
797
+ submittedAt: r.submitted_at,
798
+ nextAction: r.next_action,
799
+ nextActionAt: r.next_action_at,
800
+ progressUrl: r.progress_url ?? "",
801
+ createdAt: r.created_at,
802
+ updatedAt: r.updated_at
803
+ };
804
+ }
805
+ __name(applicationRow, "applicationRow");
806
+ function stageRow(r) {
807
+ return { id: r.id, applicationId: r.application_id, kind: r.kind, scheduledAt: r.scheduled_at, location: r.location, link: r.link, result: r.result, notes: r.notes };
808
+ }
809
+ __name(stageRow, "stageRow");
810
+ function proposalRow(r) {
811
+ return {
812
+ id: r.id,
813
+ kind: r.kind,
814
+ payload: json.parse(r.payload_json, null),
815
+ payloadHash: r.payload_hash,
816
+ createdBy: r.created_by,
817
+ reason: r.reason,
818
+ decision: r.decision,
819
+ expiresAt: r.expires_at,
820
+ createdAt: r.created_at,
821
+ ...r.execution ? { execution: r.execution, executionStartedAt: r.execution_started_at } : {}
822
+ };
823
+ }
824
+ __name(proposalRow, "proposalRow");
825
+ var NOT_UNDONE = "(如果已经做了一部分,那部分不会撤回,请核对后再点)";
826
+ var CAMPAIGN_SELECT = "select c.*, co.name as company_name from campaign c join company co on co.id = c.company_id";
827
+ var Domain = class {
828
+ constructor(db) {
829
+ this.db = db;
830
+ const blank = db.prepare("select id, name from company where norm = ''").all();
831
+ for (const r of blank) db.prepare("update company set norm = ? where id = ?").run(companyKey(r.name), r.id);
832
+ if (blank.length) for (const g of db.prepare("select norm from company group by norm having count(*) > 1").all()) {
833
+ const [keep, ...rest] = db.prepare("select id from company where norm = ? order by created_at, id").all(g.norm);
834
+ for (const r of rest) this.mergeCompany(r.id, keep.id);
835
+ }
836
+ }
837
+ db;
838
+ static {
839
+ __name(this, "Domain");
840
+ }
841
+ // ---------- company / campaign / position ----------
842
+ upsertCompany(name, type = "", industry = "") {
843
+ return upsertCompany(this.db, name, type, industry);
844
+ }
845
+ /** Fold `fromId` into `intoId`: campaigns and tags move, the old name becomes an alias so later scans land on the survivor. */
846
+ mergeCompany(fromId, intoId) {
847
+ if (fromId === intoId) throw new DomainError("SAME_COMPANY");
848
+ const from = this.db.prepare("select * from company where id = ?").get(fromId);
849
+ const into = this.db.prepare("select * from company where id = ?").get(intoId);
850
+ if (!from || !into) throw new NotFound(`company ${from ? intoId : fromId}`);
851
+ return tx(this.db, () => {
852
+ const aliases = Array.from(/* @__PURE__ */ new Set([...json.parse(into.aliases, []), from.name, ...json.parse(from.aliases, [])]));
853
+ const moved = this.db.prepare("update campaign set company_id = ? where company_id = ?").run(intoId, fromId).changes;
854
+ this.db.prepare("update or ignore recommendation set company_id = ? where company_id = ?").run(intoId, fromId);
855
+ this.db.prepare("delete from recommendation where company_id = ?").run(fromId);
856
+ this.db.prepare("update or ignore company_intel set company_id = ? where company_id = ?").run(intoId, fromId);
857
+ this.db.prepare("delete from company_intel where company_id = ?").run(fromId);
858
+ this.db.prepare("update company set aliases = ? where id = ?").run(json.str(aliases), intoId);
859
+ this.db.prepare("delete from company where id = ?").run(fromId);
860
+ return { ok: true, movedCampaigns: moved };
861
+ });
862
+ }
863
+ upsertCampaign(c) {
864
+ return upsertCampaign(this.db, c);
865
+ }
866
+ upsertPosition(campaignId, title, extra = {}) {
867
+ return upsertPosition(this.db, campaignId, title, extra);
868
+ }
869
+ listCampaigns(q) {
870
+ const where = [];
871
+ const args = [];
872
+ const today = localDay();
873
+ if (q.deadlineWithinDays != null) {
874
+ const until = /* @__PURE__ */ new Date();
875
+ until.setDate(until.getDate() + Math.floor(q.deadlineWithinDays));
876
+ where.push("c.closes_at is not null and c.closes_at >= ? and c.closes_at <= ?");
877
+ args.push(today, localDay(until));
878
+ }
879
+ if (q.city) {
880
+ where.push("c.cities like ?");
881
+ args.push(`%${q.city}%`);
882
+ }
883
+ if (q.grade) {
884
+ where.push("c.source_grade = ?");
885
+ args.push(q.grade);
886
+ }
887
+ for (const word of (q.query ?? "").split(/\s+/).filter(Boolean)) {
888
+ where.push("(co.name like ? or c.major_req like ? or c.batch like ? or exists (select 1 from position p where p.campaign_id = c.id and p.title like ?))");
889
+ const like = `%${word}%`;
890
+ args.push(like, like, like, like);
891
+ }
892
+ const sql = `${CAMPAIGN_SELECT} ${where.length ? "where " + where.join(" and ") : ""} order by case when c.closes_at >= ? then 0 when c.closes_at is null then 1 else 2 end, c.closes_at asc, c.updated_at desc`;
893
+ const rows = this.db.prepare(sql).all(...args, today);
894
+ const items = rows.slice(0, q.limit ?? 50).map(campaignRow);
895
+ if (!items.length) return { items, total: rows.length };
896
+ const ctx = loadContext(this.db, [...new Set(items.map((c) => c.companyId))]);
897
+ return { items: items.map((c) => ({ ...c, deadline: campaignSaying(ctx, c.id) })), total: rows.length };
898
+ }
899
+ getCampaign(id) {
900
+ const row = this.db.prepare(`${CAMPAIGN_SELECT} where c.id = ?`).get(id);
901
+ if (!row) throw new NotFound(`campaign ${id}`);
902
+ const positions = this.db.prepare("select * from position where campaign_id = ? order by title").all(id).map(positionRow);
903
+ const applications = this.db.prepare("select * from application where campaign_id = ?").all(id).map(applicationRow);
904
+ return { campaign: campaignRow(row), positions, applications };
905
+ }
906
+ listPositions(q) {
907
+ const where = [];
908
+ const args = [];
909
+ if (q.campaignId) {
910
+ where.push("p.campaign_id = ?");
911
+ args.push(q.campaignId);
912
+ }
913
+ if (q.companyId) {
914
+ where.push("c.company_id = ?");
915
+ args.push(q.companyId);
916
+ }
917
+ if (q.watchedOnly) where.push("p.watch_priority is not null and p.watch_priority > 0");
918
+ for (const word of (q.query ?? "").split(/\s+/).filter(Boolean)) {
919
+ where.push("(p.title like ? or co.name like ?)");
920
+ args.push(`%${word}%`, `%${word}%`);
921
+ }
922
+ const anyOf = /* @__PURE__ */ __name((field, words) => {
923
+ const ws = words.split(/[\s,,、|]+/).filter(Boolean);
924
+ if (ws.length) {
925
+ where.push(`(${ws.map(() => `${field} like ?`).join(" or ")})`);
926
+ args.push(...ws.map((w) => `%${w}%`));
927
+ }
928
+ }, "anyOf");
929
+ anyOf("coalesce(nullif(p.major_req, ''), c.major_req)", q.major ?? "");
930
+ anyOf("coalesce(nullif(p.degree_req, ''), c.degree_req)", q.degree ?? "");
931
+ if (q.city) {
932
+ where.push("((case when p.cities != '[]' then p.cities else c.cities end) like ?)");
933
+ args.push(`%${q.city}%`);
934
+ }
935
+ where.push("p.status != 'closed'");
936
+ const from = `from position p join campaign c on c.id = p.campaign_id join company co on co.id = c.company_id ${where.length ? "where " + where.join(" and ") : ""} order by c.closes_at is null, c.closes_at asc, co.name`;
937
+ const full = `select p.*, co.name as company_name, c.closes_at, c.deadline_kind, c.apply_url, c.notice_url, c.major_req as campaign_major_req, (select a.state from application a where a.position_id = p.id and a.state != 'cancelled') as app_state`;
938
+ const limit = q.limit ?? 100;
939
+ if (q.includeExpired) return { items: this.db.prepare(`${full} ${from} limit ?`).all(...args, limit).map(positionRow) };
940
+ const matches = this.db.prepare(`select p.id, p.campaign_id, p.closes_at, c.company_id ${from}`).iterate(...args);
941
+ const open = [];
942
+ try {
943
+ for (let n = Math.max(limit * 3, 300), done = false; !done && open.length < limit; n *= 2) {
944
+ const chunk = [];
945
+ while (chunk.length < n) {
946
+ const r = matches.next();
947
+ if (r.done) {
948
+ done = true;
949
+ break;
950
+ }
951
+ chunk.push(r.value);
952
+ }
953
+ const expired = expiredMany(this.db, chunk);
954
+ for (const p of chunk) if (!expired.get(p.id) && open.length < limit) open.push(p.id);
955
+ }
956
+ } finally {
957
+ matches.return?.();
958
+ }
959
+ const byId = /* @__PURE__ */ new Map();
960
+ for (let i = 0; i < open.length; i += 5e3) {
961
+ const part = open.slice(i, i + 5e3);
962
+ for (const r of this.db.prepare(`${full} from position p join campaign c on c.id = p.campaign_id join company co on co.id = c.company_id where p.id in (${part.map(() => "?").join(",")})`).all(...part)) byId.set(r.id, r);
963
+ }
964
+ return { items: open.map((id) => positionRow(byId.get(id))) };
965
+ }
966
+ watchPosition(positionId, priority, note = "") {
967
+ const r = this.db.prepare("update position set watch_priority = ?, watch_note = ?, updated_at = ? where id = ?").run(priority === 0 ? null : priority, note, now(), positionId);
968
+ if (r.changes === 0) throw new NotFound(`position ${positionId}`);
969
+ }
970
+ // ---------- application ----------
971
+ createApplication(positionId, channel = "", note = "", source = "manual") {
972
+ const pos = this.db.prepare("select campaign_id from position where id = ?").get(positionId);
973
+ if (!pos) throw new NotFound(`position ${positionId}`);
974
+ const old = this.db.prepare("select id, state from application where position_id = ?").get(positionId);
975
+ if (old?.state === "cancelled") return this.updateApplication({ id: old.id, state: "draft", closeReason: "", note }, source);
976
+ if (old) throw new DomainError("ALREADY_EXISTS");
977
+ const id = ulid2();
978
+ const t = now();
979
+ return tx(this.db, () => {
980
+ this.db.prepare("insert into application(id, position_id, campaign_id, state, channel, created_at, updated_at) values (?,?,?,?,?,?,?)").run(id, positionId, pos.campaign_id, "draft", channel, t, t);
981
+ this.event(id, "created", note, source);
982
+ return this.getApplication(id);
983
+ });
984
+ }
985
+ listApplications(state) {
986
+ const rows = this.db.prepare(`select a.*, p.title, p.cities, p.jd_text, co.name as company_name, c.closes_at, c.apply_url, c.notice_url from application a
987
+ join position p on p.id = a.position_id join campaign c on c.id = a.campaign_id join company co on co.id = c.company_id
988
+ where ${state ? "a.state = ?" : "a.state != 'cancelled'"} order by a.updated_at desc`).all(...state ? [state] : []);
989
+ return {
990
+ items: rows.map((r) => ({
991
+ application: applicationRow(r),
992
+ companyName: r.company_name,
993
+ title: r.title,
994
+ closesAt: r.closes_at ?? null,
995
+ applyUrl: r.apply_url,
996
+ noticeUrl: r.notice_url ?? "",
997
+ cities: json.parse(r.cities, []),
998
+ jdText: r.jd_text ?? "",
999
+ stages: this.db.prepare("select * from stage where application_id = ? order by scheduled_at is null, scheduled_at").all(r.id).map(stageRow),
1000
+ lastEvent: this.db.prepare("select at, kind, detail, source from application_event where application_id = ? order by at desc, rowid desc limit 1").get(r.id) ?? null
1001
+ }))
1002
+ };
1003
+ }
1004
+ getApplication(id) {
1005
+ const r = this.db.prepare("select * from application where id = ?").get(id);
1006
+ if (!r) throw new NotFound(`application ${id}`);
1007
+ return applicationRow(r);
1008
+ }
1009
+ updateApplication(p, source = "manual") {
1010
+ const cur = this.getApplication(p.id);
1011
+ return tx(this.db, () => {
1012
+ if (p.state && p.state !== cur.state) {
1013
+ if (!ALLOWED[cur.state]?.includes(p.state)) throw new DomainError("INVALID_TRANSITION", `${cur.state} → ${p.state}`);
1014
+ this.event(p.id, "state", `${cur.state} → ${p.state}${p.note ? ": " + p.note : ""}`, source);
1015
+ } else if (p.note) this.event(p.id, "note", p.note, source);
1016
+ if (p.state === "cancelled" && cur.state !== "cancelled") this.db.prepare("update position set watch_priority = null, watch_note = ?, updated_at = ? where id = ?").run(`不投了:${p.closeReason ?? ""}`, now(), cur.positionId);
1017
+ const submittedAt = p.submittedAt ?? (p.state === "submitted" && !cur.submittedAt ? now() : cur.submittedAt);
1018
+ this.db.prepare(`update application set resume_label = ?, state = ?, close_reason = ?, external_app_id = ?, submitted_at = ?, next_action = ?, next_action_at = ?, progress_url = ?, updated_at = ? where id = ?`).run(p.resumeLabel ?? cur.resumeLabel, p.state ?? cur.state, p.closeReason ?? cur.closeReason, p.externalAppId ?? cur.externalAppId, submittedAt, p.nextAction ?? cur.nextAction, p.nextActionAt ?? cur.nextActionAt, p.progressUrl ?? cur.progressUrl ?? "", now(), p.id);
1019
+ return this.getApplication(p.id);
1020
+ });
1021
+ }
1022
+ event(applicationId, kind, detail = "", source = "manual") {
1023
+ this.db.prepare("insert into application_event(id, application_id, at, kind, detail, source) values (?,?,?,?,?,?)").run(ulid2(), applicationId, now(), kind, detail, source);
1024
+ }
1025
+ timeline(id) {
1026
+ const application = this.getApplication(id);
1027
+ const stages = this.db.prepare("select * from stage where application_id = ? order by scheduled_at").all(id).map(stageRow);
1028
+ const events = this.db.prepare("select at, kind, detail, source from application_event where application_id = ? order by at, rowid").all(id);
1029
+ return { application, stages, events };
1030
+ }
1031
+ addStage(p, source = "manual") {
1032
+ this.getApplication(p.applicationId);
1033
+ const id = ulid2();
1034
+ return tx(this.db, () => {
1035
+ this.db.prepare("insert into stage(id, application_id, kind, scheduled_at, location, link, notes, created_at) values (?,?,?,?,?,?,?,?)").run(id, p.applicationId, p.kind, p.scheduledAt ?? null, p.location ?? "", p.link ?? "", p.notes ?? "", now());
1036
+ this.event(p.applicationId, "stage", `${p.kind}${p.scheduledAt ? " @ " + p.scheduledAt : ""}`, source);
1037
+ if (["draft", "submitted"].includes(this.getApplication(p.applicationId).state)) {
1038
+ this.db.prepare("update application set state = 'in_process', updated_at = ? where id = ?").run(now(), p.applicationId);
1039
+ }
1040
+ return stageRow(this.db.prepare("select * from stage where id = ?").get(id));
1041
+ });
1042
+ }
1043
+ /** Reschedule, record a result ("通过" / "已取消"), or fix the link of an existing stage. */
1044
+ updateStage(p, source = "manual") {
1045
+ const cur = this.db.prepare("select * from stage where id = ?").get(p.id);
1046
+ if (!cur) throw new NotFound(`stage ${p.id}`);
1047
+ return tx(this.db, () => {
1048
+ this.db.prepare("update stage set scheduled_at = ?, result = ?, link = ?, location = ?, notes = ? where id = ?").run(p.scheduledAt ?? cur.scheduled_at, p.result ?? cur.result, p.link ?? cur.link, p.location ?? cur.location, p.notes ?? cur.notes, p.id);
1049
+ const what = [p.scheduledAt && p.scheduledAt !== cur.scheduled_at && `改到 ${p.scheduledAt}`, p.result && p.result !== cur.result && `结果:${p.result}`].filter(Boolean).join(",");
1050
+ if (what) this.event(cur.application_id, "stage", `${cur.kind} ${what}`, source);
1051
+ return stageRow(this.db.prepare("select * from stage where id = ?").get(p.id));
1052
+ });
1053
+ }
1054
+ // ---------- proposals ----------
1055
+ createProposal(kind, payload, createdBy, reason = "", ttlHours = 72, turnId) {
1056
+ const id = ulid2();
1057
+ const payloadJson = JSON.stringify(payload);
1058
+ const expires = new Date(Date.now() + ttlHours * 36e5).toISOString();
1059
+ this.db.prepare("insert into proposal(id, kind, payload_json, payload_hash, created_by, reason, decision, expires_at, created_at, turn_id) values (?,?,?,?,?,?,?,?,?,?)").run(id, kind, payloadJson, sha(payloadJson), createdBy, reason, "pending", expires, now(), turnId ?? null);
1060
+ return proposalRow(this.db.prepare("select * from proposal where id = ?").get(id));
1061
+ }
1062
+ listProposals(decision = "pending") {
1063
+ return this.db.prepare("select * from proposal where decision = ? order by created_at desc").all(decision).map(proposalRow);
1064
+ }
1065
+ /**
1066
+ * Approve = verify hash + not expired + run the executor (passed by the registry). A card counts as approved only when
1067
+ * its work is done (ADR-062):
1068
+ * - an executor that answers at once: run and marked in one transaction — its database writes and the mark stand or
1069
+ * fall together;
1070
+ * - one that answers later (reads a page, a table; returns a Promise): the claim ('running') is committed with the
1071
+ * work's first synchronous part, the card stays pending meanwhile, and the result settles it — approved with what
1072
+ * the work returned, or back to waiting with the reason. What the work already did is not undone; the reason says so.
1073
+ * A card left 'running' by a stopped daemon is 'interrupted' at the next start (interruptRunningProposals) and runs
1074
+ * again only when the person approves it again.
1075
+ */
1076
+ decideProposal(id, decision, payloadHash, execute) {
1077
+ const r = this.db.prepare("select * from proposal where id = ?").get(id);
1078
+ if (!r) throw new NotFound(`proposal ${id}`);
1079
+ if (r.decision !== "pending") throw new DomainError("ALREADY_DECIDED");
1080
+ if (r.execution === "running") throw new DomainError("RUNNING", "这张卡正在执行,等它做完");
1081
+ if (r.payload_hash !== payloadHash) throw new DomainError("HASH_MISMATCH");
1082
+ if (r.expires_at < now()) {
1083
+ this.db.prepare("update proposal set decision = 'expired', decided_at = ?, execution = null, execution_started_at = null where id = ?").run(now(), id);
1084
+ throw new DomainError("EXPIRED");
1085
+ }
1086
+ const settle = /* @__PURE__ */ __name((result) => this.db.prepare("update proposal set decision = ?, decided_at = ?, result_json = ?, execution = null, execution_started_at = null where id = ?").run(decision, now(), json.str(result ?? null), id), "settle");
1087
+ const started = tx(this.db, () => {
1088
+ if (decision === "rejected") {
1089
+ settle(void 0);
1090
+ return { ok: true, result: void 0 };
1091
+ }
1092
+ this.db.prepare("update proposal set execution = 'running', execution_started_at = ? where id = ?").run(now(), id);
1093
+ const result = execute(r.kind, JSON.parse(r.payload_json));
1094
+ if (!(result instanceof Promise)) settle(result);
1095
+ return { ok: true, result };
1096
+ });
1097
+ if (!(started.result instanceof Promise)) return started;
1098
+ return started.result.then(
1099
+ (result) => {
1100
+ settle(result);
1101
+ return { ok: true, result };
1102
+ },
1103
+ (e) => {
1104
+ this.db.prepare("update proposal set execution = null, execution_started_at = null where id = ? and execution = 'running'").run(id);
1105
+ if (e instanceof Error && e.code !== "MODEL_DELEGATED") e.message += NOT_UNDONE;
1106
+ throw e;
1107
+ }
1108
+ );
1109
+ }
1110
+ /** At start: a card whose work was running when the daemon stopped is 'interrupted' — never run again by itself (ADR-062). */
1111
+ interruptRunningProposals() {
1112
+ return Number(this.db.prepare("update proposal set execution = 'interrupted' where execution = 'running'").run().changes);
1113
+ }
1114
+ // ---------- today ----------
1115
+ /** 今日: the days coming up and what is unconfirmed, for the posts the person holds (ADR-041), plus what waits on them. */
1116
+ today(days = 7) {
1117
+ const { deadlines, verify } = deadlineBoard(this.db, days);
1118
+ const pendingProposals = this.db.prepare("select count(*) as n from proposal where decision = 'pending'").get().n;
1119
+ const nextActions = this.db.prepare(`select a.id, a.next_action, a.next_action_at, co.name as company_name from application a join campaign c on c.id = a.campaign_id join company co on co.id = c.company_id
1120
+ where a.next_action != '' and a.state not in ('closed', 'cancelled') order by a.next_action_at is null, a.next_action_at`).all().map((r) => ({ applicationId: r.id, companyName: r.company_name, nextAction: r.next_action, nextActionAt: r.next_action_at }));
1121
+ const workflowsRunning = this.db.prepare("select count(*) as n from workflow where state = 'running'").get().n;
1122
+ const reminders = this.db.prepare("select count(*) as n from workflow where state = 'running' and kind = 'deadline_reminder'").get().n;
1123
+ return { deadlines, verify, pendingProposals, nextActions, workflowsRunning, reminders };
1124
+ }
1125
+ audit(actor, tool, input, ok, detail = "") {
1126
+ this.db.prepare("insert into audit(id, at, actor, tool, input_json, ok, detail) values (?,?,?,?,?,?,?)").run(ulid2(), now(), actor, tool, json.str(input), ok ? 1 : 0, detail);
1127
+ poolUnrelatedWrite(this.db);
1128
+ }
1129
+ };
1130
+
1131
+ export {
1132
+ HARD_FIELDS,
1133
+ NotFound,
1134
+ DomainError,
1135
+ twinsFrom,
1136
+ sayDate,
1137
+ loadContext,
1138
+ expiredMany,
1139
+ resolveFor,
1140
+ campaignSaying,
1141
+ openCampaigns,
1142
+ campaignClosesAt,
1143
+ recordFact,
1144
+ setOverride,
1145
+ poolDirty,
1146
+ refreshPool,
1147
+ deadlineBoard,
1148
+ listPool,
1149
+ poolWatched,
1150
+ searchCompanies,
1151
+ poolFacets,
1152
+ scanHistory,
1153
+ Domain
1154
+ };