@mazjq/zhiro 0.1.2 → 0.2.1

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.
@@ -12,6 +12,7 @@ import {
12
12
  callData,
13
13
  checkPoints,
14
14
  codexSpec,
15
+ companyFinancials,
15
16
  dataFile,
16
17
  describeBackground,
17
18
  docxHtml,
@@ -21,7 +22,6 @@ import {
21
22
  extractNotice,
22
23
  extractText,
23
24
  fetchArticle,
24
- getIntel,
25
25
  getIntelDetail,
26
26
  importSme,
27
27
  keepData,
@@ -29,34 +29,41 @@ import {
29
29
  loadImages,
30
30
  loadSources,
31
31
  mergeFacts,
32
+ normName,
32
33
  noticeToListings,
33
34
  oneshot,
34
35
  readData,
36
+ readIntel,
35
37
  readRun,
36
38
  readZipEntry,
37
39
  reextract,
38
40
  refIndex,
39
41
  refreshIntel,
42
+ resolveNeeq,
40
43
  resumePending,
41
44
  scanFolder,
45
+ smeMatch,
42
46
  smeRun,
43
47
  spawnRun,
44
48
  startSmeResearch,
45
49
  tools,
46
50
  workerStatus
47
- } from "./chunk-YJNBYSVM.js";
51
+ } from "./chunk-2XJ45GUM.js";
48
52
  import {
49
53
  dshSpec
50
- } from "./chunk-ZL2RKXOF.js";
54
+ } from "./chunk-ZUUYRVX6.js";
51
55
  import {
52
56
  FACT_KINDS,
53
57
  SEP,
54
58
  SKILL_FILE,
55
59
  UI_DIST,
60
+ chooseModel,
56
61
  claudeSpec,
57
62
  cliBackend,
58
63
  factLabels,
59
64
  followedModel,
65
+ harnessModel,
66
+ harnessModels,
60
67
  listActions,
61
68
  listFacts,
62
69
  listPresets,
@@ -82,7 +89,7 @@ import {
82
89
  undoAction,
83
90
  undoPlan,
84
91
  upsertFact
85
- } from "./chunk-UMCKC7XP.js";
92
+ } from "./chunk-VOUILWMI.js";
86
93
  import "./chunk-OOHBM3BT.js";
87
94
  import {
88
95
  UpgradeFailed,
@@ -92,13 +99,13 @@ import {
92
99
  stampApp,
93
100
  upgradeVault,
94
101
  zipRead
95
- } from "./chunk-J2D7DYGW.js";
102
+ } from "./chunk-A722HITH.js";
96
103
  import {
97
104
  VERSION
98
- } from "./chunk-NNH4ZG6F.js";
105
+ } from "./chunk-H3EFVYGD.js";
99
106
  import {
100
107
  WorkflowEngine
101
- } from "./chunk-LUKCNS5M.js";
108
+ } from "./chunk-GRZ4VMSO.js";
102
109
  import {
103
110
  asAgentCall,
104
111
  createLlm,
@@ -109,7 +116,7 @@ import {
109
116
  replyJob,
110
117
  usageSummary,
111
118
  withPurpose
112
- } from "./chunk-F6DLONN7.js";
119
+ } from "./chunk-7IV2NG2D.js";
113
120
  import {
114
121
  Domain,
115
122
  DomainError,
@@ -131,7 +138,7 @@ import {
131
138
  scanHistory,
132
139
  searchCompanies,
133
140
  setOverride
134
- } from "./chunk-LH25QWMP.js";
141
+ } from "./chunk-ZKD2BKN3.js";
135
142
  import {
136
143
  SYNC_REASONS,
137
144
  addSource,
@@ -163,14 +170,14 @@ import {
163
170
  sourceScan,
164
171
  sources,
165
172
  splitList
166
- } from "./chunk-YYNUPNBH.js";
173
+ } from "./chunk-4UWMDKMH.js";
167
174
  import {
168
175
  SchemaMismatch,
169
176
  json,
170
177
  now,
171
178
  openDb,
172
179
  tx
173
- } from "./chunk-5KCN32HQ.js";
180
+ } from "./chunk-4EPNDXRS.js";
174
181
  import {
175
182
  clearConfigTemps,
176
183
  configFile,
@@ -492,16 +499,16 @@ function scheduleAllDeadlines(s) {
492
499
  const db2 = s.domain.db;
493
500
  const ids = db2.prepare(`select p.id from position p where ${HELD}`).all().map((r) => r.id);
494
501
  const resolved = resolveFor(db2, ids);
495
- const keep = /* @__PURE__ */ new Set();
502
+ const keep2 = /* @__PURE__ */ new Set();
496
503
  for (const id of ids) {
497
504
  const r = resolved.get(id);
498
505
  if (!r || r.expired || !r.date && !asap(r)) continue;
499
506
  const key = `deadline_reminder:${id}:${r.date ?? "asap"}`;
500
- keep.add(key);
507
+ keep2.add(key);
501
508
  s.workflows.start("deadline_reminder", id, key);
502
509
  }
503
- for (const w of db2.prepare("select id, idempotency_key k from workflow where kind = 'deadline_reminder' and state = 'running'").all()) if (!keep.has(w.k)) db2.prepare("delete from workflow where id = ?").run(w.id);
504
- return keep.size;
510
+ for (const w of db2.prepare("select id, idempotency_key k from workflow where kind = 'deadline_reminder' and state = 'running'").all()) if (!keep2.has(w.k)) db2.prepare("delete from workflow where id = ?").run(w.id);
511
+ return keep2.size;
505
512
  }
506
513
  __name(scheduleAllDeadlines, "scheduleAllDeadlines");
507
514
 
@@ -509,6 +516,515 @@ __name(scheduleAllDeadlines, "scheduleAllDeadlines");
509
516
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readFileSync as readFileSync3 } from "node:fs";
510
517
  import { join as join3 } from "node:path";
511
518
  import { ulid } from "ulid";
519
+
520
+ // apps/daemon/src/service/employer.ts
521
+ var UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/128.0 Safari/537.36";
522
+ var F10 = "https://datacenter.eastmoney.com/securities/api/data/v1/get";
523
+ var A_SHARE = ["深交所主板A股", "上交所主板A股", "深交所创业板A股", "上交所科创板A股", "北京证券交易所A股", "深交所风险警示板A股", "上交所风险警示板A股"];
524
+ var KEEP_DAYS = 7;
525
+ var sleep = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "sleep");
526
+ async function f10(report, columns, filter, page = 1) {
527
+ const url = `${F10}?reportName=${report}&columns=${columns}&filter=${encodeURIComponent(filter)}&pageNumber=${page}&pageSize=500&source=HSF10&client=PC`;
528
+ for (let k = 0; ; k++) {
529
+ try {
530
+ const res = await fetch(url, { headers: { "user-agent": UA }, signal: AbortSignal.timeout(2e4) });
531
+ const x = await res.json();
532
+ if (!x?.success && x?.code !== 9201) throw new Error(`东方财富 ${report}:${x?.message ?? res.status}`);
533
+ return { data: x?.result?.data ?? [], pages: x?.result?.pages ?? 0 };
534
+ } catch (e) {
535
+ if (k >= 1) throw e;
536
+ await sleep(1500);
537
+ }
538
+ }
539
+ }
540
+ __name(f10, "f10");
541
+ async function f10All(report, columns, filter) {
542
+ const first = await f10(report, columns, filter);
543
+ const out = [...first.data];
544
+ for (let p = 2; p <= first.pages; p++) {
545
+ await sleep(300);
546
+ out.push(...(await f10(report, columns, filter, p)).data);
547
+ }
548
+ return out;
549
+ }
550
+ __name(f10All, "f10All");
551
+ function cached(db2, key, now3) {
552
+ const r = db2.prepare("select fetched_at, data_json from peer_snapshot where key = ?").get(key);
553
+ if (!r || now3 - Date.parse(r.fetched_at) > KEEP_DAYS * 864e5) return null;
554
+ return { at: r.fetched_at, data: JSON.parse(r.data_json) };
555
+ }
556
+ __name(cached, "cached");
557
+ var keep = /* @__PURE__ */ __name((db2, key, data, now3) => db2.prepare("insert or replace into peer_snapshot(key, fetched_at, data_json) values (?,?,?)").run(key, new Date(now3).toISOString(), JSON.stringify(data)), "keep");
558
+ async function remembered(db2, key, now3, look) {
559
+ const r = db2.prepare("select fetched_at, data_json from peer_snapshot where key = ?").get(key);
560
+ if (r && now3 - Date.parse(r.fetched_at) <= KEEP_DAYS * 864e5) return JSON.parse(r.data_json).v;
561
+ const v = await look();
562
+ keep(db2, key, { v }, now3);
563
+ return v;
564
+ }
565
+ __name(remembered, "remembered");
566
+ async function universe(db2, now3 = Date.now()) {
567
+ const hit = cached(db2, "universe:3", now3);
568
+ if (hit) return hit;
569
+ const rows = await f10All("RPT_F10_BASIC_ORGINFO", "SECUCODE,SECURITY_NAME_ABBR,ORG_NAME,BOARD_NAME_LEVEL,EMP_NUM,INDUSTRYCSRC1", `(SECURITY_TYPE in (${A_SHARE.map((t) => `"${t}"`).join(",")}))`);
570
+ const data = rows.filter((r) => r.BOARD_NAME_LEVEL).map((r) => ({ code: r.SECUCODE, name: r.SECURITY_NAME_ABBR ?? "", org: r.ORG_NAME ?? "", industry: r.BOARD_NAME_LEVEL, employees: Number(r.EMP_NUM) > 0 ? Number(r.EMP_NUM) : null, csrc: r.INDUSTRYCSRC1 ?? "" }));
571
+ keep(db2, "universe:3", data, now3);
572
+ return { at: new Date(now3).toISOString(), data };
573
+ }
574
+ __name(universe, "universe");
575
+ function latestYear(now3 = Date.now()) {
576
+ const d = new Date(now3 + 8 * 36e5);
577
+ return d.getUTCMonth() >= 4 ? d.getUTCFullYear() - 1 : d.getUTCFullYear() - 2;
578
+ }
579
+ __name(latestYear, "latestYear");
580
+ var sector = /* @__PURE__ */ __name((industry) => industry.split("-")[0], "sector");
581
+ function peerGroup(c) {
582
+ if (c.peerSw) return { key: c.peerSw, label: `申万一级「${c.peerSw}」`, has: /* @__PURE__ */ __name((x) => sector(x.industry) === c.peerSw, "has") };
583
+ if (c.market === "新三板" && c.csrc) return { key: `csrc:${c.csrc}`, label: `证监会行业「${c.csrc}」`, has: /* @__PURE__ */ __name((x) => x.csrc === c.csrc, "has") };
584
+ const top = sector(c.industry);
585
+ return { key: top, label: `申万一级「${top}」`, has: /* @__PURE__ */ __name((x) => sector(x.industry) === top, "has") };
586
+ }
587
+ __name(peerGroup, "peerGroup");
588
+ async function industryPool(db2, group, year, all, now3 = Date.now()) {
589
+ const key = `industry:${group.key}:${year}`;
590
+ const hit = cached(db2, key, now3);
591
+ if (hit) return hit;
592
+ const members = all.filter(group.has);
593
+ const rows = new Map(members.map((c) => [c.code, { ...c, years: {} }]));
594
+ await statements(rows, year);
595
+ const data = [...rows.values()].filter((r) => (r.years[String(year)]?.revenue ?? 0) > 0);
596
+ keep(db2, key, data, now3);
597
+ return { at: new Date(now3).toISOString(), data };
598
+ }
599
+ __name(industryPool, "industryPool");
600
+ async function statements(rows, year) {
601
+ const members = [...rows.values()];
602
+ const dates = [year, year - 1, year - 2, year - 3].map((y) => `'${y}-12-31'`).join(",");
603
+ const row2 = /* @__PURE__ */ __name((code, date) => {
604
+ const r = rows.get(code), y = date.slice(0, 4);
605
+ return r.years[y] ??= { revenue: null, netProfit: null, rd: null, opCost: null, staffCash: null, dividendCash: null, capex: null };
606
+ }, "row");
607
+ const num = /* @__PURE__ */ __name((v) => v === null || v === void 0 || v === "" || !Number.isFinite(Number(v)) ? null : Number(v), "num");
608
+ for (let i = 0; i < members.length; i += 100) {
609
+ const codes = `(SECUCODE in (${members.slice(i, i + 100).map((c) => `"${c.code}"`).join(",")}))(REPORT_DATE in (${dates}))`;
610
+ for (const r of await f10All("RPT_F10_FINANCE_GINCOME", "SECUCODE,REPORT_DATE,TOTAL_OPERATE_INCOME,PARENT_NETPROFIT,RESEARCH_EXPENSE,OPERATE_COST", codes)) {
611
+ if (!rows.has(r.SECUCODE)) continue;
612
+ Object.assign(row2(r.SECUCODE, r.REPORT_DATE), { revenue: num(r.TOTAL_OPERATE_INCOME), netProfit: num(r.PARENT_NETPROFIT), rd: num(r.RESEARCH_EXPENSE), opCost: num(r.OPERATE_COST) });
613
+ }
614
+ await sleep(300);
615
+ for (const r of await f10All("RPT_F10_FINANCE_GCASHFLOW", "SECUCODE,REPORT_DATE,PAY_STAFF_CASH,ASSIGN_DIVIDEND_PORFIT,CONSTRUCT_LONG_ASSET", codes)) {
616
+ if (!rows.has(r.SECUCODE)) continue;
617
+ Object.assign(row2(r.SECUCODE, r.REPORT_DATE), { staffCash: num(r.PAY_STAFF_CASH), dividendCash: num(r.ASSIGN_DIVIDEND_PORFIT), capex: num(r.CONSTRUCT_LONG_ASSET) });
618
+ }
619
+ await sleep(300);
620
+ }
621
+ }
622
+ __name(statements, "statements");
623
+ async function neeqCompany(name) {
624
+ const hit = await resolveNeeq(name, normName(name));
625
+ if (!hit) return null;
626
+ const r = (await f10("RPT_F10_BASIC_ORGINFO", "SECUCODE,SECURITY_NAME_ABBR,ORG_NAME,BOARD_NAME_LEVEL,EMP_NUM,INDUSTRYCSRC1", `(SECUCODE="${hit.code}.NQ")`)).data[0];
627
+ return r?.BOARD_NAME_LEVEL ? { code: r.SECUCODE, name: r.SECURITY_NAME_ABBR ?? hit.name, org: r.ORG_NAME ?? "", industry: r.BOARD_NAME_LEVEL, employees: Number(r.EMP_NUM) > 0 ? Number(r.EMP_NUM) : null, market: "新三板", csrc: r.INDUSTRYCSRC1 ?? "" } : null;
628
+ }
629
+ __name(neeqCompany, "neeqCompany");
630
+ async function overseas(name) {
631
+ const f = await companyFinancials({ name });
632
+ if (f.status === "failed") throw new Error(`${name}:认出了上市公司但数据没取成`);
633
+ const sec = f.status === "listed" ? f.security : null;
634
+ if (!sec || sec.market !== "港股" && sec.market !== "美股") return null;
635
+ const hk = sec.market === "港股", code = hk ? `${sec.code}.HK` : sec.code;
636
+ const main = (await f10(hk ? "RPT_HKF10_FN_MAININDICATOR" : "RPT_USF10_FN_GMAININDICATOR", "ALL", `(SECUCODE="${code}")`)).data;
637
+ const annual = main.filter((r) => hk ? /年报/.test(String(r.REPORT_TYPE)) : r.DATE_TYPE === "年报");
638
+ await sleep(300);
639
+ const rd = (await f10(hk ? "RPT_HKF10_FN_INCOME_PC" : "RPT_USF10_FN_INCOME", "ALL", hk ? `(SECUCODE="${code}")(STD_ITEM_NAME="研发费用")(DATE_TYPE_CODE="001")` : `(SECUCODE="${code}")(REPORT_TYPE="年报")(ITEM_NAME="研发费用")`).catch(() => ({ data: [] }))).data;
640
+ const rdOn = new Map(rd.map((r) => [String(r.REPORT_DATE).slice(0, 10), Number(r.AMOUNT)]));
641
+ const years = {};
642
+ for (const r of annual) {
643
+ const y = String(hk ? r.REPORT_TYPE : r.REPORT_DATA_TYPE).match(/\d{4}/)?.[0];
644
+ if (!y || years[y]) continue;
645
+ const num = /* @__PURE__ */ __name((v) => v === null || v === void 0 || v === "" || !Number.isFinite(Number(v)) ? null : Number(v), "num");
646
+ const revenue = num(r.OPERATE_INCOME), gm = num(r.GROSS_PROFIT_RATIO);
647
+ years[y] = { revenue, netProfit: num(hk ? r.HOLDER_PROFIT : r.PARENT_HOLDER_NETPROFIT), rd: rdOn.get(String(r.REPORT_DATE).slice(0, 10)) ?? null, opCost: revenue !== null && gm !== null ? revenue * (1 - gm / 100) : null, staffCash: null, dividendCash: null, capex: null };
648
+ }
649
+ return { code, name: sec.name, org: sec.name, industry: f.basic?.industry ?? "", employees: f.basic?.employees ?? null, market: sec.market, approx: !!f.approx, years };
650
+ }
651
+ __name(overseas, "overseas");
652
+ function unlistedFacts(db2, name) {
653
+ const n = normName(name);
654
+ const boss = n.length >= 2 ? db2.prepare("select brand, stage, scale, industry, last_seen from boss_candidate order by last_seen desc limit 2000").all().find((b) => b.brand && normName(b.brand) === n) : void 0;
655
+ const sme = smeMatch(db2, name).matches[0];
656
+ const co = db2.prepare("select id from company where name = ? or norm = ?").get(name, n);
657
+ const ipo = co ? db2.prepare("select data_json, fetched_at from company_intel where company_id = ? and kind = 'ipo'").get(co.id) : void 0;
658
+ const szse = ipo ? JSON.parse(ipo.data_json)?.szse : null;
659
+ return {
660
+ ...boss ? { boss: { stage: boss.stage, scale: boss.scale, industry: boss.industry, seen: String(boss.last_seen).slice(0, 10) } } : {},
661
+ ...sme ? { sme: sme.level } : {},
662
+ ...szse ? { ipo: { board: "深交所", stage: szse.stage, date: szse.updatedAt || szse.acceptedAt || "" } } : {}
663
+ };
664
+ }
665
+ __name(unlistedFacts, "unlistedFacts");
666
+ var FACETS = [
667
+ { key: "growth", label: "成长", measure: "营收三年复合增速" },
668
+ { key: "rd", label: "研发", measure: "研发费用占营收" },
669
+ { key: "pay", label: "待遇信号", measure: "每名员工对应的职工现金支出", note: "粗指标:含奖金、社保、离职补偿,员工数取最新" },
670
+ { key: "profit", label: "盈利", measure: "归母净利润占营收" }
671
+ ];
672
+ var ratio = /* @__PURE__ */ __name((a, b) => a === null || a === void 0 || !b || b <= 0 ? null : a / b, "ratio");
673
+ var change = /* @__PURE__ */ __name((a, b) => a === null || a === void 0 || b === null || b === void 0 || b === 0 ? null : (a - b) / Math.abs(b), "change");
674
+ function measures(r, year) {
675
+ const y = r.years[String(year)] ?? {}, p = r.years[String(year - 1)] ?? {}, b = r.years[String(year - 3)];
676
+ const growth = b && (b.revenue ?? 0) > 0 && (y.revenue ?? 0) > 0 ? (y.revenue / b.revenue) ** (1 / 3) - 1 : null;
677
+ return {
678
+ main: { growth, rd: ratio(y.rd, y.revenue), pay: ratio(y.staffCash, r.employees), profit: ratio(y.netProfit, y.revenue) },
679
+ more: {
680
+ revenue: y.revenue ?? null,
681
+ netProfit: y.netProfit ?? null,
682
+ prevNetProfit: p.netProfit ?? null,
683
+ employees: r.employees,
684
+ revenueChange: change(y.revenue, p.revenue),
685
+ profitChange: change(y.netProfit, p.netProfit),
686
+ staffCashChange: change(y.staffCash, p.staffCash),
687
+ rdChange: change(y.rd, p.rd),
688
+ grossMargin: y.revenue && y.opCost !== null && y.opCost !== void 0 ? (y.revenue - y.opCost) / y.revenue : null,
689
+ capexToRevenue: ratio(y.capex, y.revenue),
690
+ dividendCashToProfit: (y.netProfit ?? 0) > 0 ? ratio(y.dividendCash, y.netProfit) : null
691
+ }
692
+ };
693
+ }
694
+ __name(measures, "measures");
695
+ function percentile(values, v) {
696
+ let below = 0, same2 = 0;
697
+ for (const x of values) {
698
+ if (x < v) below++;
699
+ else if (x === v) same2++;
700
+ }
701
+ return values.length ? Math.round((below + same2 / 2) / values.length * 100) : null;
702
+ }
703
+ __name(percentile, "percentile");
704
+ function moved(m) {
705
+ const out = [], up = /* @__PURE__ */ __name((x) => x !== null && x > 0.02, "up"), down = /* @__PURE__ */ __name((x) => x !== null && x < -0.02, "down");
706
+ const turned = m.prevNetProfit !== null && m.netProfit !== null ? m.prevNetProfit < 0 && m.netProfit > 0 ? "扭亏为盈" : m.prevNetProfit > 0 && m.netProfit < 0 ? "由盈转亏" : null : null;
707
+ if (turned) out.push(`营收${up(m.revenueChange) ? "增长" : down(m.revenueChange) ? "下降" : "基本持平"},${turned}`);
708
+ else if (up(m.revenueChange) && up(m.profitChange)) out.push("营收和利润同时增长");
709
+ else if (up(m.revenueChange) && down(m.profitChange)) out.push("营收增长、利润下降(增收不增利)");
710
+ else if (down(m.revenueChange) && up(m.profitChange)) out.push(`营收下降、利润增长${down(m.staffCashChange) || down(m.rdChange) ? `;同期${[down(m.staffCashChange) && "职工现金", down(m.rdChange) && "研发费用"].filter(Boolean).join("、")}减少,利润可能来自压缩成本(待核实)` : ""}`);
711
+ else if (down(m.revenueChange) && down(m.profitChange)) out.push("营收和利润同时下降");
712
+ if (up(m.profitChange) && down(m.staffCashChange) && !down(m.revenueChange)) out.push("利润增长的同时职工现金减少(可能减员或降薪,待核实)");
713
+ if (m.netProfit !== null && m.netProfit < 0 && turned !== "由盈转亏") out.push("当年亏损");
714
+ return out;
715
+ }
716
+ __name(moved, "moved");
717
+ function labels(pct) {
718
+ const out = [], at = /* @__PURE__ */ __name((k, n) => (pct[k] ?? -1) >= n, "at");
719
+ if (at("rd", 75) && at("growth", 50)) out.push({ label: "研发驱动", rule: "研发百分位 ≥ 75 且成长 ≥ 50" });
720
+ else if (at("growth", 75)) out.push({ label: "高成长", rule: "成长百分位 ≥ 75" });
721
+ if (at("profit", 75) && pct.growth !== null && pct.growth < 50) out.push({ label: "稳健盈利", rule: "盈利百分位 ≥ 75 且成长 < 50" });
722
+ if (at("pay", 75)) out.push({ label: "职工现金支出靠前", rule: "待遇信号百分位 ≥ 75" });
723
+ return out;
724
+ }
725
+ __name(labels, "labels");
726
+ var SCENARIOS = {
727
+ 成长研发优先: { growth: 0.35, rd: 0.35, pay: 0.15, profit: 0.15 },
728
+ 均衡: { growth: 0.25, rd: 0.25, pay: 0.25, profit: 0.25 },
729
+ 稳定回报优先: { growth: 0.15, rd: 0.15, pay: 0.35, profit: 0.35 }
730
+ };
731
+ var KEYS = FACETS.map((f) => f.key);
732
+ function rng(seed) {
733
+ let a = seed >>> 0;
734
+ return () => {
735
+ a = a + 1831565813 >>> 0;
736
+ let t = a;
737
+ t = Math.imul(t ^ t >>> 15, t | 1);
738
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
739
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
740
+ };
741
+ }
742
+ __name(rng, "rng");
743
+ function sampleWeights(center, spread, n, seed = 1) {
744
+ const r = rng(seed), out = [];
745
+ for (let tries = 0; out.length < n && tries < n * 2e3; tries++) {
746
+ const e = KEYS.map(() => -Math.log(1 - r())), s = e.reduce((a, b) => a + b, 0);
747
+ const w = Object.fromEntries(KEYS.map((k, i) => [k, e[i] / s]));
748
+ if (KEYS.every((k) => Math.abs(w[k] - center[k]) <= spread)) out.push(w);
749
+ }
750
+ return out;
751
+ }
752
+ __name(sampleWeights, "sampleWeights");
753
+ var score = /* @__PURE__ */ __name((p, w, fill) => KEYS.reduce((a, k) => a + w[k] * (p[k] ?? fill ?? 0), 0), "score");
754
+ var rankOf = /* @__PURE__ */ __name((mine, others) => 1 + others.filter((x) => x > mine).length, "rankOf");
755
+ var q = /* @__PURE__ */ __name((xs, f) => {
756
+ const s = [...xs].sort((a, b) => a - b);
757
+ return s[Math.min(s.length - 1, Math.floor(f * s.length))];
758
+ }, "q");
759
+ function placeUnder(picks, center, spread, n = 2e3, seed = 1) {
760
+ const complete = picks.filter((p) => KEYS.every((k) => p.pct[k] !== null));
761
+ const samples = sampleWeights(center, spread, n, seed);
762
+ const ranks = new Map(complete.map((p) => [p.code, []]));
763
+ for (const w of samples) {
764
+ const s = complete.map((p) => score(p.pct, w));
765
+ complete.forEach((p, i) => ranks.get(p.code).push(rankOf(s[i], s.filter((_, j) => j !== i))));
766
+ }
767
+ const at = complete.map((p) => score(p.pct, center));
768
+ const out = /* @__PURE__ */ new Map();
769
+ complete.forEach((p, i) => {
770
+ const r = ranks.get(p.code);
771
+ out.set(p.code, { place: rankOf(at[i], at.filter((_, j) => j !== i)), usually: [q(r, 0.1), q(r, 0.9)], top3Share: r.length && complete.length > 3 ? Math.round(r.filter((x) => x <= 3).length / r.length * 20) * 5 : null });
772
+ });
773
+ for (const p of picks.filter((x) => !complete.includes(x))) {
774
+ const known = KEYS.filter((k) => p.pct[k] !== null), wsum = known.reduce((a, k) => a + center[k], 0);
775
+ const cond = wsum ? known.reduce((a, k) => a + center[k] / wsum * p.pct[k], 0) : null;
776
+ out.set(p.code, {
777
+ place: null,
778
+ byKnown: cond === null ? null : rankOf(cond, at),
779
+ best: rankOf(score(p.pct, center, 100), at),
780
+ worst: rankOf(score(p.pct, center, 0), at),
781
+ missing: KEYS.filter((k) => p.pct[k] === null)
782
+ });
783
+ }
784
+ return { samples: samples.length, places: out };
785
+ }
786
+ __name(placeUnder, "placeUnder");
787
+ function notOutdone(picks) {
788
+ const complete = picks.filter((p) => KEYS.every((k) => p.pct[k] !== null));
789
+ const beats = /* @__PURE__ */ __name((a, b) => KEYS.every((k) => a.pct[k] >= b.pct[k]) && KEYS.some((k) => a.pct[k] > b.pct[k]), "beats");
790
+ return new Map(complete.map((p) => [p.code, complete.filter((o) => o !== p && beats(o, p)).map((o) => o.code)]));
791
+ }
792
+ __name(notOutdone, "notOutdone");
793
+ function findCompany(all, query) {
794
+ const s = query.trim();
795
+ const code = s.match(/^(\d{6})(\.(SZ|SH|BJ))?$/i);
796
+ if (code) return all.find((c) => c.code.startsWith(code[1])) ?? null;
797
+ const exact = all.filter((c) => c.name === s || c.org === s);
798
+ if (exact.length) return exact[0];
799
+ const n = normName(s);
800
+ const near = n.length >= 2 ? all.filter((c) => normName(c.org) === n || c.name.replace(/\s/g, "") === n || normName(c.org).includes(n)) : [];
801
+ return near.length === 1 ? near[0] : near.length ? { candidates: near.slice(0, 5) } : null;
802
+ }
803
+ __name(findCompany, "findCompany");
804
+ async function compare(db2, q2, now3 = Date.now()) {
805
+ const year = q2.year ?? latestYear(now3);
806
+ const uni = await universe(db2, now3);
807
+ const found = [], notFound = [];
808
+ const elsewhere = [];
809
+ for (const name of q2.companies.slice(0, 15)) {
810
+ const c = findCompany(uni.data, name) ?? await remembered(db2, `neeq:${name}`, now3, () => neeqCompany(name)).catch(() => null);
811
+ if (!c) elsewhere.push(name);
812
+ else if ("candidates" in c) notFound.push({ query: name, reason: "有几家名字相近,用股票代码再查", candidates: c.candidates.map((x) => `${x.name} ${x.code}`) });
813
+ else if (!found.some((f) => f.code === c.code)) found.push(c);
814
+ }
815
+ const tops = found.filter((c) => !c.market).map((c) => sector(c.industry));
816
+ const peerSw = q2.peerIndustry?.trim() || [...new Set(tops)].sort((a, b) => tops.filter((x) => x === b).length - tops.filter((x) => x === a).length)[0];
817
+ const abroad = /* @__PURE__ */ new Map();
818
+ for (const name of elsewhere) {
819
+ const r = await remembered(db2, `abroad:${name}`, now3, () => overseas(name)).catch(() => null);
820
+ if (!r) notFound.push({ query: name, reason: "不是 A 股、新三板、港股或美股上市公司(或名字写法不同);未上市公司没有公开财报,放不进这张表", known: unlistedFacts(db2, name) });
821
+ else if (!peerSw) notFound.push({ query: name, reason: `${r.market}公司没有申万行业:和 A 股公司一起比,或给出拿哪个申万一级行业当同行(peerIndustry)` });
822
+ else if (!uni.data.some((c) => sector(c.industry) === peerSw)) notFound.push({ query: name, reason: `没有叫「${peerSw}」的申万一级行业` });
823
+ else if (!found.some((f) => f.code === r.code)) {
824
+ r.peerSw = peerSw;
825
+ abroad.set(r.code, r);
826
+ found.push(r);
827
+ }
828
+ }
829
+ const pools = /* @__PURE__ */ new Map();
830
+ for (const c of found) {
831
+ const g = peerGroup(c);
832
+ if (!pools.has(g.label)) pools.set(g.label, await industryPool(db2, g, year, uni.data, now3));
833
+ }
834
+ const outside = new Map(found.filter((c) => c.market === "新三板").map((c) => [c.code, { ...c, years: {} }]));
835
+ if (outside.size) await statements(outside, year);
836
+ const rows = found.map((c) => {
837
+ const pool = pools.get(peerGroup(c).label);
838
+ const other = outside.get(c.code) ?? abroad.get(c.code);
839
+ const me = pool.data.find((r) => r.code === c.code) ?? (other?.years[String(year)]?.revenue ? other : void 0);
840
+ if (!me) return { company: c, missingYear: true };
841
+ const m = measures(me, year);
842
+ const all = pool.data.map((r) => measures(r, year).main);
843
+ const pct = Object.fromEntries(KEYS.map((k) => {
844
+ const v = m.main[k];
845
+ return [k, v === null ? null : percentile(all.map((x) => x[k]).filter((x) => x !== null), v)];
846
+ }));
847
+ const n = Object.fromEntries(KEYS.map((k) => [k, all.filter((x) => x[k] !== null).length]));
848
+ return { company: c, years: me.years, value: m.main, pct, peers: n, more: m.more, moved: moved(m.more), labels: labels(pct), coverage: KEYS.filter((k) => m.main[k] !== null).length };
849
+ });
850
+ const ok = rows.filter((r) => "pct" in r);
851
+ const picks = ok.map((r) => ({ code: r.company.code, pct: r.pct }));
852
+ const spread = Math.max(0.02, Math.min(1, q2.spread ?? 0.1));
853
+ const scenarios = { ...SCENARIOS };
854
+ if (q2.weights) {
855
+ const w = Object.fromEntries(KEYS.map((k) => [k, Math.max(0, q2.weights[k] ?? 0)])), s = KEYS.reduce((a, k) => a + w[k], 0);
856
+ if (s > 0) scenarios.你设的权重 = Object.fromEntries(KEYS.map((k) => [k, w[k] / s]));
857
+ }
858
+ const under = Object.fromEntries(Object.entries(scenarios).map(([name, w]) => [name, { weights: w, ...placeUnder(picks, w, spread) }]));
859
+ const any = placeUnder(picks, SCENARIOS.均衡, 1, 4e3, 2);
860
+ const outdone = notOutdone(picks);
861
+ return {
862
+ year,
863
+ peerGroup: "同一行业的全部 A 股上市公司(当年有营收的):A 股公司按申万一级,新三板公司按证监会行业",
864
+ data: { universeAt: uni.at, pools: Object.fromEntries([...pools].map(([k, v]) => [k, { at: v.at, companies: v.data.length }])), source: "东方财富 F10:公司概况、利润表、现金流量表(年报)" },
865
+ measures: FACETS,
866
+ companies: rows.map((r) => {
867
+ if (!("pct" in r)) return { code: r.company.code, name: r.company.name, org: r.company.org, industry: r.company.industry, market: r.company.market ?? "A股", note: /^[A-Z]/.test(r.company.code) ? `在申报上市(申报代码 ${r.company.code}),还没有公开的年报` : `没有 ${year} 年报数据(可能是当年之后才上市)` };
868
+ return {
869
+ code: r.company.code,
870
+ name: r.company.name,
871
+ org: r.company.org,
872
+ industry: r.company.industry,
873
+ market: r.company.market ?? "A股",
874
+ peerGroup: peerGroup(r.company).label,
875
+ ...r.company.market === "新三板" ? { yardstick: "新三板公司,按同一证监会行业的 A 股上市公司来量" } : {},
876
+ .../^[A-Z]/.test(r.company.code) ? { yardstick: `在申报上市(申报代码 ${r.company.code}),财务数据来自申报材料,按同行业 A 股上市公司来量` } : {},
877
+ ...r.company.peerSw ? { yardstick: `${r.company.market}公司,按申万一级「${r.company.peerSw}」的 A 股上市公司来量;不披露支付给职工的现金,待遇信号缺;金额是公司财报自己的币种,比较的都是比例,不受币种影响${r.company.approx ? `;「${r.company.name}」是按名字相近认的,请核对是不是你要的那家` : ""}` } : {},
878
+ years: r.years,
879
+ value: r.value,
880
+ percentile: r.pct,
881
+ peers: r.peers,
882
+ coverage: `${r.coverage}/4`,
883
+ more: r.more,
884
+ movedTogether: r.moved,
885
+ labels: r.labels,
886
+ outdoneBy: outdone.has(r.company.code) ? outdone.get(r.company.code).map((c) => ok.find((x) => x.company.code === c).company.name) : null,
887
+ under: Object.fromEntries(Object.entries(under).map(([k, v]) => [k, v.places.get(r.company.code)])),
888
+ anyWeighting: any.places.get(r.company.code)?.top3Share ?? null
889
+ };
890
+ }),
891
+ scenarios: Object.fromEntries(Object.entries(under).map(([k, v]) => [k, { weights: v.weights, samples: v.samples }])),
892
+ spread,
893
+ notFound,
894
+ method: `每项先在本行业同行里算百分位(越高越好),名次只在这几家之间排。「通常第 a–b 名」是在所选权重上下各 ${Math.round(spread * 100)} 个百分点内随机抽权重(固定种子)时名次的 10%–90% 范围;「进前三的比例」是这些权重里排进前三的占比,是偏好情景的比例,不是现实概率。缺数据的不估值,只给按已知指标的名次和最好 / 最坏情况。`
895
+ };
896
+ }
897
+ __name(compare, "compare");
898
+ var yi = /* @__PURE__ */ __name((v) => v === null || v === void 0 ? "—" : `${(v / 1e8).toFixed(2)} 亿`, "yi");
899
+ var pc = /* @__PURE__ */ __name((v) => v === null || v === void 0 ? "—" : `${(v * 100).toFixed(1)}%`, "pc");
900
+ function factsMarkdown(c, r) {
901
+ if (!c.years) return `## 一、事实(职路取数)
902
+
903
+ ${c.note ?? "不是 A 股上市公司,职路没有它的同行数据。"}
904
+ `;
905
+ const ys = Object.keys(c.years).sort().reverse().slice(0, 4);
906
+ const row2 = /* @__PURE__ */ __name((label2, f) => `| ${label2} | ${ys.map((y) => f(c.years[y])).join(" | ")} |`, "row");
907
+ const pool = r.data.pools[c.peerGroup];
908
+ return [
909
+ `## 一、事实(职路取数,${r.year} 年报口径)`,
910
+ "",
911
+ `${c.name}(${c.code})· ${c.industry} · 员工 ${c.more.employees ?? "—"} 人(最新)`,
912
+ "",
913
+ `| 年份 | ${ys.join(" | ")} |`,
914
+ `|---|${ys.map(() => "---").join("|")}|`,
915
+ row2("营业总收入", (y) => yi(y.revenue)),
916
+ row2("归母净利润", (y) => yi(y.netProfit)),
917
+ row2("研发费用", (y) => yi(y.rd)),
918
+ row2("支付给职工的现金", (y) => yi(y.staffCash)),
919
+ row2("分配股利、利润或偿付利息的现金", (y) => yi(y.dividendCash)),
920
+ row2("购建长期资产的现金", (y) => yi(y.capex)),
921
+ "",
922
+ `在${c.peerGroup} ${pool?.companies ?? "?"} 家 A 股公司里的位置(百分位越高越好)${c.yardstick ? `;${c.yardstick}` : ""}:`,
923
+ "",
924
+ ...FACETS.map((f) => `- ${f.label}(${f.measure}):${f.key === "pay" ? c.value.pay === null ? "—" : `${(c.value.pay / 1e4).toFixed(1)} 万/人` : pc(c.value[f.key])},同行第 ${c.percentile[f.key] ?? "—"} 百分位${f.note ? `(${f.note})` : ""}`),
925
+ `- 毛利率 ${pc(c.more.grossMargin)};购建长期资产占营收 ${pc(c.more.capexToRevenue)};分配股利和偿付利息的现金占净利 ${pc(c.more.dividendCashToProfit)}(含利息,不全是分红)`,
926
+ ...c.movedTogether.length ? [`- ${r.year} 年同期一起发生的:${c.movedTogether.join(";")}`] : [],
927
+ ...c.labels.length ? [`- 规则标签:${c.labels.map((l) => `${l.label}(${l.rule})`).join(";")}`] : [],
928
+ "",
929
+ `来源:${r.data.source},取数 ${pool?.at?.slice(0, 10) ?? "?"}。`,
930
+ ""
931
+ ].join("\n");
932
+ }
933
+ __name(factsMarkdown, "factsMarkdown");
934
+
935
+ // apps/daemon/src/service/fit.ts
936
+ import { parseJsonWithRepair, contentText } from "@earendil-works/pi-ai";
937
+ var SYSTEM = `你是校招岗位匹配评估员。给你一位应届生的画像和一个岗位,逐条对照岗位的硬性要求(学历、专业、技能、经验、证书、地点)和工作内容,判断他能不能投、合不合适。
938
+ 只依据给出的内容,不要编造画像里没有的经历;岗位没写的要求不算缺口。
939
+ 只输出 JSON:{"score": 0-100 的整数, "verdict": "一句话结论(20 字内)", "meets": ["满足的要求,每条带画像里的依据"], "gaps": ["不满足或存疑的要求,每条写明差在哪"]}
940
+ 打分:硬性要求有明确不满足的不超过 40;硬性要求都满足且工作内容与其方向/研究/项目直接相关 80 以上;满足但方向只部分相关 55–79。`;
941
+ function profileText(s) {
942
+ return listFacts(s.domain.db).items.filter((f) => f.kind !== "exclude").map((f) => `- ${f.kind}:${f.statement || Object.values(f.data ?? {}).join(" / ")}`).join("\n");
943
+ }
944
+ __name(profileText, "profileText");
945
+ function postText(s, positionId) {
946
+ const p = s.domain.db.prepare(`select p.title, p.jd_text, p.degree_req, p.major_req, p.cities, c.degree_req c_degree, c.major_req c_major, c.cities c_cities, c.source_kind, co.name, co.type
947
+ from position p join campaign c on c.id = p.campaign_id join company co on co.id = c.company_id where p.id = ?`).get(positionId);
948
+ if (!p) return null;
949
+ const agg = isAggregate(p.source_kind);
950
+ return `公司:${p.name}(${p.type || "类型未知"})
951
+ 岗位:${p.title}
952
+ 地点:${json.parse(p.cities, []).join("、") || json.parse(p.c_cities, []).join("、") || "未写"}
953
+ 学历要求:${p.degree_req || p.c_degree || "未写"}
954
+ 专业要求:${p.major_req || (agg ? p.c_major : "") || "未写"}
955
+ JD:
956
+ ${(p.jd_text || "(来源没有提供 JD 正文,只能按岗位名判断)").slice(0, 5e3)}`;
957
+ }
958
+ __name(postText, "postText");
959
+ async function deepFit(s, positionId) {
960
+ if (!s.llm) throw new Error("没有配置模型");
961
+ const post = postText(s, positionId);
962
+ if (!post) return null;
963
+ const profile = profileText(s);
964
+ const msg = await withPurpose("fit.deep", positionId, () => s.llm.models.completeSimple(s.llm.model(), { systemPrompt: SYSTEM, messages: [{ role: "user", content: [{ type: "text", text: `## 画像
965
+ ${profile}
966
+
967
+ ## 岗位
968
+ ${post}` }], timestamp: Date.now() }] }, { reasoning: "low" }));
969
+ const o = parseJsonWithRepair(contentText(msg.content).trim().replace(/^```(?:json)?\s*|\s*```$/g, ""));
970
+ const list2 = /* @__PURE__ */ __name((v) => Array.isArray(v) ? v.map(String).slice(0, 10) : [], "list");
971
+ const score2 = Math.max(0, Math.min(100, Math.round(Number(o.score) || 0)));
972
+ s.domain.db.prepare(`insert into position_fit(position_id, score, verdict, meets, gaps, model, profile_hash, created_at) values (?,?,?,?,?,?,?,?)
973
+ on conflict(position_id) do update set score = excluded.score, verdict = excluded.verdict, meets = excluded.meets, gaps = excluded.gaps, model = excluded.model, profile_hash = excluded.profile_hash, created_at = excluded.created_at`).run(positionId, score2, String(o.verdict ?? "").slice(0, 60), json.str(list2(o.meets)), json.str(list2(o.gaps)), String(s.llm.model()?.id ?? ""), sha(profile), now());
974
+ return score2;
975
+ }
976
+ __name(deepFit, "deepFit");
977
+ var queue = [];
978
+ var failed = /* @__PURE__ */ new Map();
979
+ var running = false;
980
+ function enqueueFit(s, ids, opts = {}) {
981
+ const hash2 = sha(profileText(s));
982
+ const fresh2 = new Set(s.domain.db.prepare(`select position_id from position_fit where profile_hash = ?`).all(hash2).map((r) => r.position_id));
983
+ const todo = ids.filter((id) => (opts.force || !fresh2.has(id)) && !queue.includes(id));
984
+ queue.push(...todo);
985
+ for (const id of todo) failed.delete(id);
986
+ void drain2(s);
987
+ return { queued: todo.length, alreadyDone: opts.force ? 0 : ids.filter((id) => fresh2.has(id)).length, pending: queue.length };
988
+ }
989
+ __name(enqueueFit, "enqueueFit");
990
+ async function drain2(s) {
991
+ if (running) return;
992
+ running = true;
993
+ try {
994
+ let waits = 0;
995
+ while (queue.length) {
996
+ const id = queue[0];
997
+ try {
998
+ await deepFit(s, id);
999
+ queue.shift();
1000
+ s.onDataChanged?.();
1001
+ } catch (e) {
1002
+ if (e?.code === "MODEL_DELEGATED" && waits < 60) {
1003
+ waits++;
1004
+ await new Promise((r) => setTimeout(r, 6e4));
1005
+ continue;
1006
+ }
1007
+ failed.set(id, String(e?.message ?? e).slice(0, 200));
1008
+ queue.shift();
1009
+ }
1010
+ }
1011
+ } finally {
1012
+ running = false;
1013
+ }
1014
+ }
1015
+ __name(drain2, "drain");
1016
+ function listFit(s, q2) {
1017
+ const hash2 = sha(profileText(s));
1018
+ const rows = q2.companyId ? s.domain.db.prepare(`select f.* from position_fit f join position p on p.id = f.position_id join campaign c on c.id = p.campaign_id where c.company_id = ?`).all(q2.companyId) : s.domain.db.prepare(`select * from position_fit where position_id in (${(q2.positionIds ?? []).map(() => "?").join(",") || "''"})`).all(...q2.positionIds ?? []);
1019
+ return {
1020
+ items: rows.map((r) => ({ positionId: r.position_id, score: r.score, verdict: r.verdict, meets: json.parse(r.meets, []), gaps: json.parse(r.gaps, []), model: r.model, at: r.created_at, stale: r.profile_hash !== hash2 })),
1021
+ pending: [...queue],
1022
+ failed: Object.fromEntries(failed)
1023
+ };
1024
+ }
1025
+ __name(listFit, "listFit");
1026
+
1027
+ // apps/daemon/src/agent/research.ts
512
1028
  var RESEARCH_SCHEMA = {
513
1029
  type: "object",
514
1030
  additionalProperties: false,
@@ -576,6 +1092,136 @@ ${GRADE_RULES}
576
1092
  最终只输出符合 schema 的 JSON(report_markdown 用中文,按上面 6 项分节,含"证据清单"一节把 evidence 再列一遍)。`;
577
1093
  }
578
1094
  __name(intelPrompt, "intelPrompt");
1095
+ var EMPLOYER_SCHEMA = {
1096
+ type: "object",
1097
+ additionalProperties: false,
1098
+ required: ["summary", "report_markdown", "evidence", "claims", "staff", "layoffs"],
1099
+ properties: {
1100
+ summary: { type: "string", description: "3 句以内:这家公司作为雇主的要点,不打分" },
1101
+ report_markdown: { type: "string", description: "中文 Markdown,从「## 二、人员:规模、人均薪资、裁员」写起:二、人员 / 三、利润为什么变 / 四、钱去了哪 / 五、激励和技术人员 / 六、上市与资本 / 七、求职信号 / 八、和你的匹配 / 九、证据清单" },
1102
+ staff: {
1103
+ type: "array",
1104
+ description: "近几年的年末员工数,每年一条,查不到的年份不写",
1105
+ items: { type: "object", additionalProperties: false, required: ["year", "employees", "source", "url"], properties: { year: { type: "number" }, employees: { type: "number" }, source: { type: "string", description: "年报「员工情况」/ 公示年报社保参保人数 / 招股书 / 其他(写明)" }, url: { type: "string" } } }
1106
+ },
1107
+ layoffs: {
1108
+ type: "array",
1109
+ description: "近三年裁员、降薪、欠薪、大规模离职的报道;没搜到就给空数组",
1110
+ items: { type: "object", additionalProperties: false, required: ["date", "what", "url"], properties: { date: { type: "string" }, what: { type: "string", description: "发生了什么、涉及哪些部门或多少人,照报道写" }, url: { type: "string" } } }
1111
+ },
1112
+ evidence: RESEARCH_SCHEMA.properties.evidence,
1113
+ claims: {
1114
+ type: "array",
1115
+ description: "报告里每条定性结论",
1116
+ items: {
1117
+ type: "object",
1118
+ additionalProperties: false,
1119
+ required: ["text", "type", "urls", "counter", "limits"],
1120
+ properties: { text: { type: "string" }, type: { type: "string", enum: ["事实", "解读", "推断"] }, urls: { type: "array", items: { type: "string" } }, counter: { type: "string", description: "找过的反面说法;没找到就写「没找到」" }, limits: { type: "string", description: "这条结论的限制" } }
1121
+ }
1122
+ }
1123
+ }
1124
+ };
1125
+ async function employerFacts(s, companyName) {
1126
+ try {
1127
+ const r = await compare(s.domain.db, { companies: [companyName] });
1128
+ const c = r.companies[0];
1129
+ return { md: c ? factsMarkdown(c, r) : factsMarkdown({ note: r.notFound[0]?.reason ?? "不是 A 股上市公司" }, r), staffCash: Object.fromEntries(Object.entries(c?.years ?? {}).map(([y, v]) => [y, v.staffCash])) };
1130
+ } catch (e) {
1131
+ return { staffCash: {}, md: `## 一、事实(职路取数)
1132
+
1133
+ 这次没取到行业数据(${String(e?.message ?? e).slice(0, 120)}),下面的数字都来自调研员给的来源。
1134
+ ` };
1135
+ }
1136
+ }
1137
+ __name(employerFacts, "employerFacts");
1138
+ function employerPrompt(s, companyName, facts) {
1139
+ const db2 = s.domain.db;
1140
+ const co = db2.prepare("select id from company where name = ?").get(companyName);
1141
+ const intel2 = co ? db2.prepare("select kind, data_json from company_intel where company_id = ? and kind in ('announcements','dividends','ipo','listing','reports')").all(co.id) : [];
1142
+ const card2 = intel2.map((r) => {
1143
+ const d = JSON.parse(r.data_json);
1144
+ if (r.kind === "listing") return d.none ? "上市:巨潮、股转、美股都没查到" : `上市:${d.market} ${d.code}`;
1145
+ if (r.kind === "ipo") return `深交所项目库:${d.szse ? `${d.szse.stage}(${d.szse.date ?? ""})` : "未检出"}`;
1146
+ if (r.kind === "dividends") return `已实施的分红:${(d.items ?? []).map((x) => `${x.period} ${x.plan}`).join(";") || "无"}`;
1147
+ if (r.kind === "announcements") return `近一年相关公告:
1148
+ ${(d.hits ?? []).filter((h) => /激励|持股|回购|减持|增持|上市|辅导|募集/.test(h.title)).slice(0, 20).map((h) => ` - ${h.date} ${h.title} ${h.url}`).join("\n") || " (无)"}`;
1149
+ if (r.kind === "reports") return `近一年研报标题:${(d.items ?? []).slice(0, 5).map((x) => x.title).join(";")}`;
1150
+ return "";
1151
+ }).filter(Boolean).join("\n");
1152
+ const posts = co ? db2.prepare("select p.title from position p join campaign c on c.id = p.campaign_id where c.company_id = ? and p.status = 'open' limit 40").all(co.id).map((p) => p.title) : [];
1153
+ const profile = profileText(s).split("\n").filter((l) => !l.startsWith("- basic")).join("\n");
1154
+ return `你是就业调研员,替一位求职者评估「${companyName}」这家雇主。今天:${now().slice(0, 10)}。
1155
+
1156
+ ## 职路已经算好的事实(报告第一节由职路放,你不用重写;引用时数字照抄,别的数字要给来源)
1157
+ ${facts}
1158
+ ## 企业情况卡里已有的
1159
+ ${card2 || "(还没有)"}
1160
+
1161
+ ## 这家公司在岗位库里在招的
1162
+ ${posts.length ? posts.join("、") : "(库里没有)"}
1163
+
1164
+ ## 求职者资料
1165
+ ${profile || "(资料是空的)"}
1166
+
1167
+ ## 要回答的(证据不够就写「依据不足」,不要编)
1168
+ 二、人员:规模、人均薪资、裁员。想知道的是:在不在裁员、人均拿多少、规模多大。
1169
+ - 近几年的年末员工数放进 staff,每年一条带来源:上市公司读年报「员工情况」的「在职员工的数量合计」;未上市的查国家企业信用信息公示系统年报里的「社保参保人数」(企查查、天眼查页面常有转载),或招股书。职路会用它和「支付给职工的现金」算每年人均,写在报告里,你不用算。
1170
+ - 搜近三年的裁员、降薪、欠薪、大规模离职报道(脉脉、新闻、黑猫投诉都可以),放进 layoffs,照报道写日期、发生了什么、涉及多少人和链接;没搜到就给空数组,在正文里写「没有搜到」,不等于没有。
1171
+ - 正文里写你对人员变化的解读:人数和工资总额怎么变的、和业务变化对得上吗。
1172
+ 三、利润为什么变:营收、成本、人数分开说。只有年报「管理层讨论与分析」或公告写明的才说是原因,否则写「同时发生」并标待核实。是不是靠减员、降薪省出来的:和第二节的人数、职工现金对照。
1173
+ 四、钱去了哪:研发、分红和回购、资本开支、并购,按上面的事实表和公告说。
1174
+ 五、激励和技术人员:近三年的股权激励、员工持股计划,读公告原文,摘出激励对象(核心技术人员几人、占员工比例)、授予价格、考核条件;写明有没有覆盖技术人员。研发人员占比、技术岗在公司里的分量。
1175
+ 六、上市与资本:已上市的写板块和近期再融资、减持;未上市的写融资轮次(天使、A、B…、战略投资)、每轮的时间、金额和主要投资方(来源:公司公告、36氪等媒体、企查查 / 天眼查公开页面,注明是哪一种),并查深交所、上交所、北交所的 IPO 项目库和证监会辅导备案,写「截至今天,以哪些名称在哪些库里没检出」,或者给出所处阶段和日期。
1176
+ 七、求职信号:在招什么方向、口碑与风险(裁员、欠薪、诉讼、减持等新闻,注明日期和来源)。
1177
+ 八、和这位求职者的匹配:优势、风险、未知各几条,单列,不并入对公司本身的判断。
1178
+
1179
+ 规则:
1180
+ - 每条定性结论放进 claims:类型(事实 / 解读 / 推断)、证据 URL、找过的反面说法(没找到写「没找到」)、限制。
1181
+ - 不打分、不给总分、不写「推荐指数」,不替求职者决定去不去。
1182
+ ${GRADE_RULES}
1183
+ 最终只输出符合 schema 的 JSON(report_markdown 从「## 二、人员:规模、人均薪资、裁员」写起,最后一节「九、证据清单」把 evidence 再列一遍)。`;
1184
+ }
1185
+ __name(employerPrompt, "employerPrompt");
1186
+ async function startEmployerReport(s, companyName) {
1187
+ const company = s.domain.db.prepare("select id from company where name = ?").get(companyName);
1188
+ const companyId = company?.id ?? s.domain.upsertCompany(companyName);
1189
+ const facts = await employerFacts(s, companyName);
1190
+ const { runId } = spawnRun(s, { runner: "claude", mode: "research.company", subjectType: "company", subjectId: companyId, subjectLabel: companyName, prompt: employerPrompt(s, companyName, facts.md), schema: EMPLOYER_SCHEMA, maxTurns: 50 });
1191
+ const workflowId = s.workflows.start("research_run", runId, `research_run:${runId}`, { companyName, companyId, prefix: facts.md, staffCash: facts.staffCash });
1192
+ return { runId, workflowId };
1193
+ }
1194
+ __name(startEmployerReport, "startEmployerReport");
1195
+ function staffMarkdown(so, staffCash = {}) {
1196
+ const staff = (so.staff ?? []).filter((x) => x.employees > 0).sort((a, b) => b.year - a.year);
1197
+ const layoffs = so.layoffs ?? [];
1198
+ if (!so.staff && !so.layoffs) return "";
1199
+ const per = /* @__PURE__ */ __name((x) => {
1200
+ const c = staffCash[String(x.year)];
1201
+ return c ? c / x.employees : null;
1202
+ }, "per");
1203
+ const wan = /* @__PURE__ */ __name((v) => v === null ? "—" : `${(v / 1e4).toFixed(1)} 万`, "wan");
1204
+ const pctOf = /* @__PURE__ */ __name((a, b) => `${a >= b ? "+" : ""}${((a - b) / b * 100).toFixed(0)}%`, "pctOf");
1205
+ const out = ["### 人员(员工数由调研员查、带来源;职工现金和人均由职路算)", ""];
1206
+ if (staff.length) {
1207
+ out.push("| 年份 | 年末员工数 | 来源 | 支付给职工的现金 | 人均 |", "|---|---|---|---|---|");
1208
+ for (const x of staff) out.push(`| ${x.year} | ${x.employees} | [${x.source}](${x.url}) | ${staffCash[String(x.year)] ? `${(staffCash[String(x.year)] / 1e8).toFixed(2)} 亿` : "—"} | ${wan(per(x))} |`);
1209
+ const [last, first] = [staff[0], staff[staff.length - 1]];
1210
+ if (staff.length > 1) {
1211
+ const pl = per(last), pf = per(first);
1212
+ out.push("", `${first.year}→${last.year}:人数 ${first.employees}→${last.employees}(${pctOf(last.employees, first.employees)})${pl && pf ? `;人均 ${wan(pf)}→${wan(pl)}(${pctOf(pl, pf)})` : ""}。人均按年末人数算,含奖金、社保和离职补偿,裁员当年会偏高。`);
1213
+ }
1214
+ } else out.push("调研员没有查到历年员工数。");
1215
+ out.push("", layoffs.length ? "裁员、降薪等报道:" : "没有搜到近三年的裁员、降薪报道(没搜到不等于没有)。", ...layoffs.map((l) => `- ${l.date} ${l.what}([来源](${l.url}))`), "");
1216
+ return out.join("\n");
1217
+ }
1218
+ __name(staffMarkdown, "staffMarkdown");
1219
+ function claimsMarkdown(so) {
1220
+ const cell3 = /* @__PURE__ */ __name((x) => String(x ?? "").replace(/\|/g, "/").replace(/\s*\n\s*/g, " "), "cell");
1221
+ const claims = so.claims ?? [];
1222
+ return claims.length ? ["", "", "## 结论核对", "", "| 结论 | 类型 | 证据 | 反面说法 | 限制 |", "|---|---|---|---|---|", ...claims.map((c) => `| ${cell3(c.text)} | ${cell3(c.type)} | ${(c.urls ?? []).map((u, i) => `[${i + 1}](${u})`).join(" ") || "无"} | ${cell3(c.counter)} | ${cell3(c.limits)} |`)].join("\n") : "";
1223
+ }
1224
+ __name(claimsMarkdown, "claimsMarkdown");
579
1225
  function startResearch(s, companyName, question = "") {
580
1226
  const company = s.domain.db.prepare("select id from company where name = ?").get(companyName);
581
1227
  const companyId = company?.id ?? s.domain.upsertCompany(companyName);
@@ -623,7 +1269,9 @@ summary: ${JSON.stringify(so.summary ?? "")}
623
1269
  ---
624
1270
 
625
1271
  `;
626
- writeFileSync3(path, header + so.report_markdown + "\n", "utf8");
1272
+ writeFileSync3(path, header + (checkpoint.prefix ? `${checkpoint.prefix}
1273
+ ${staffMarkdown(so, checkpoint.staffCash)}
1274
+ ` : "") + so.report_markdown + claimsMarkdown(so) + "\n", "utf8");
627
1275
  const ins = s.domain.db.prepare("insert into evidence(id, subject_type, subject_id, grade, url, title, quote, fetched_at, report_path, run_id) values (?,?,?,?,?,?,?,?,?,?)");
628
1276
  for (const e of so.evidence ?? []) ins.run(ulid(), "company", checkpoint.companyId, e.grade ?? "推断", e.url ?? "", e.title ?? "", e.quote ?? "", now(), path, runId);
629
1277
  s.domain.db.prepare("update agent_run set status = 'done', output_path = ?, summary = ?, session_id = ?, cost_usd = ?, finished_at = ? where id = ?").run(path, String(so.summary ?? "").slice(0, 500), r.sessionId ?? "", r.costUsd ?? 0, now(), runId);
@@ -971,98 +1619,6 @@ import { join as join5 } from "node:path";
971
1619
  import { ulid as ulid3 } from "ulid";
972
1620
  import YAML from "yaml";
973
1621
 
974
- // apps/daemon/src/service/fit.ts
975
- import { parseJsonWithRepair, contentText } from "@earendil-works/pi-ai";
976
- var SYSTEM = `你是校招岗位匹配评估员。给你一位应届生的画像和一个岗位,逐条对照岗位的硬性要求(学历、专业、技能、经验、证书、地点)和工作内容,判断他能不能投、合不合适。
977
- 只依据给出的内容,不要编造画像里没有的经历;岗位没写的要求不算缺口。
978
- 只输出 JSON:{"score": 0-100 的整数, "verdict": "一句话结论(20 字内)", "meets": ["满足的要求,每条带画像里的依据"], "gaps": ["不满足或存疑的要求,每条写明差在哪"]}
979
- 打分:硬性要求有明确不满足的不超过 40;硬性要求都满足且工作内容与其方向/研究/项目直接相关 80 以上;满足但方向只部分相关 55–79。`;
980
- function profileText(s) {
981
- return listFacts(s.domain.db).items.filter((f) => f.kind !== "exclude").map((f) => `- ${f.kind}:${f.statement || Object.values(f.data ?? {}).join(" / ")}`).join("\n");
982
- }
983
- __name(profileText, "profileText");
984
- function postText(s, positionId) {
985
- const p = s.domain.db.prepare(`select p.title, p.jd_text, p.degree_req, p.major_req, p.cities, c.degree_req c_degree, c.major_req c_major, c.cities c_cities, c.source_kind, co.name, co.type
986
- from position p join campaign c on c.id = p.campaign_id join company co on co.id = c.company_id where p.id = ?`).get(positionId);
987
- if (!p) return null;
988
- const agg = isAggregate(p.source_kind);
989
- return `公司:${p.name}(${p.type || "类型未知"})
990
- 岗位:${p.title}
991
- 地点:${json.parse(p.cities, []).join("、") || json.parse(p.c_cities, []).join("、") || "未写"}
992
- 学历要求:${p.degree_req || p.c_degree || "未写"}
993
- 专业要求:${p.major_req || (agg ? p.c_major : "") || "未写"}
994
- JD:
995
- ${(p.jd_text || "(来源没有提供 JD 正文,只能按岗位名判断)").slice(0, 5e3)}`;
996
- }
997
- __name(postText, "postText");
998
- async function deepFit(s, positionId) {
999
- if (!s.llm) throw new Error("没有配置模型");
1000
- const post = postText(s, positionId);
1001
- if (!post) return null;
1002
- const profile = profileText(s);
1003
- const msg = await withPurpose("fit.deep", positionId, () => s.llm.models.completeSimple(s.llm.model(), { systemPrompt: SYSTEM, messages: [{ role: "user", content: [{ type: "text", text: `## 画像
1004
- ${profile}
1005
-
1006
- ## 岗位
1007
- ${post}` }], timestamp: Date.now() }] }, { reasoning: "low" }));
1008
- const o = parseJsonWithRepair(contentText(msg.content).trim().replace(/^```(?:json)?\s*|\s*```$/g, ""));
1009
- const list2 = /* @__PURE__ */ __name((v) => Array.isArray(v) ? v.map(String).slice(0, 10) : [], "list");
1010
- const score = Math.max(0, Math.min(100, Math.round(Number(o.score) || 0)));
1011
- s.domain.db.prepare(`insert into position_fit(position_id, score, verdict, meets, gaps, model, profile_hash, created_at) values (?,?,?,?,?,?,?,?)
1012
- on conflict(position_id) do update set score = excluded.score, verdict = excluded.verdict, meets = excluded.meets, gaps = excluded.gaps, model = excluded.model, profile_hash = excluded.profile_hash, created_at = excluded.created_at`).run(positionId, score, String(o.verdict ?? "").slice(0, 60), json.str(list2(o.meets)), json.str(list2(o.gaps)), String(s.llm.model()?.id ?? ""), sha(profile), now());
1013
- return score;
1014
- }
1015
- __name(deepFit, "deepFit");
1016
- var queue = [];
1017
- var failed = /* @__PURE__ */ new Map();
1018
- var running = false;
1019
- function enqueueFit(s, ids, opts = {}) {
1020
- const hash2 = sha(profileText(s));
1021
- const fresh2 = new Set(s.domain.db.prepare(`select position_id from position_fit where profile_hash = ?`).all(hash2).map((r) => r.position_id));
1022
- const todo = ids.filter((id) => (opts.force || !fresh2.has(id)) && !queue.includes(id));
1023
- queue.push(...todo);
1024
- for (const id of todo) failed.delete(id);
1025
- void drain2(s);
1026
- return { queued: todo.length, alreadyDone: opts.force ? 0 : ids.filter((id) => fresh2.has(id)).length, pending: queue.length };
1027
- }
1028
- __name(enqueueFit, "enqueueFit");
1029
- async function drain2(s) {
1030
- if (running) return;
1031
- running = true;
1032
- try {
1033
- let waits = 0;
1034
- while (queue.length) {
1035
- const id = queue[0];
1036
- try {
1037
- await deepFit(s, id);
1038
- queue.shift();
1039
- s.onDataChanged?.();
1040
- } catch (e) {
1041
- if (e?.code === "MODEL_DELEGATED" && waits < 60) {
1042
- waits++;
1043
- await new Promise((r) => setTimeout(r, 6e4));
1044
- continue;
1045
- }
1046
- failed.set(id, String(e?.message ?? e).slice(0, 200));
1047
- queue.shift();
1048
- }
1049
- }
1050
- } finally {
1051
- running = false;
1052
- }
1053
- }
1054
- __name(drain2, "drain");
1055
- function listFit(s, q) {
1056
- const hash2 = sha(profileText(s));
1057
- const rows = q.companyId ? s.domain.db.prepare(`select f.* from position_fit f join position p on p.id = f.position_id join campaign c on c.id = p.campaign_id where c.company_id = ?`).all(q.companyId) : s.domain.db.prepare(`select * from position_fit where position_id in (${(q.positionIds ?? []).map(() => "?").join(",") || "''"})`).all(...q.positionIds ?? []);
1058
- return {
1059
- items: rows.map((r) => ({ positionId: r.position_id, score: r.score, verdict: r.verdict, meets: json.parse(r.meets, []), gaps: json.parse(r.gaps, []), model: r.model, at: r.created_at, stale: r.profile_hash !== hash2 })),
1060
- pending: [...queue],
1061
- failed: Object.fromEntries(failed)
1062
- };
1063
- }
1064
- __name(listFit, "listFit");
1065
-
1066
1622
  // apps/daemon/src/service/same-company.ts
1067
1623
  var GROUPS = [
1068
1624
  [/^(中国船舶(重工|工业)?集团(有限公司)?|中国船舶|中船集团|中船)/, "中船"],
@@ -1665,9 +2221,9 @@ ${m.text}`, "utf8");
1665
2221
  return { message: msgRow(db2.prepare("select * from message where id = ?").get(id)), added: true };
1666
2222
  }
1667
2223
  __name(ingestMessage, "ingestMessage");
1668
- function listMessages(db2, q = {}) {
1669
- const where = q.category ? "where category = ?" : q.includeOther ? "" : "where category != '其他'";
1670
- const rows = db2.prepare(`select * from message ${where} order by received_at desc limit ?`).all(...q.category ? [q.category] : [], q.limit ?? 100);
2224
+ function listMessages(db2, q2 = {}) {
2225
+ const where = q2.category ? "where category = ?" : q2.includeOther ? "" : "where category != '其他'";
2226
+ const rows = db2.prepare(`select * from message ${where} order by received_at desc limit ?`).all(...q2.category ? [q2.category] : [], q2.limit ?? 100);
1671
2227
  return { items: rows.map(msgRow) };
1672
2228
  }
1673
2229
  __name(listMessages, "listMessages");
@@ -1897,9 +2453,9 @@ function pickResume(db2, positionId) {
1897
2453
  const inTitle = x.roleKeywords.filter((k) => title.includes(low2(k)));
1898
2454
  const inJd = x.roleKeywords.filter((k) => !title.includes(low2(k)) && jd.includes(low2(k)));
1899
2455
  const types = x.companyTypes.filter((t) => ctype.includes(low2(t)) || industry.includes(low2(t)));
1900
- const score = 3 * inTitle.length + Math.min(inJd.length, 3) + (types.length ? 2 : 0);
2456
+ const score2 = 3 * inTitle.length + Math.min(inJd.length, 3) + (types.length ? 2 : 0);
1901
2457
  const reason = [inTitle.length && `岗位名含 ${inTitle.join("/")}`, inJd.length && `JD 提到 ${inJd.join("/")}`, types.length && `公司类型 ${types.join("/")}`].filter(Boolean).join(";");
1902
- return { resume: x, score, reason };
2458
+ return { resume: x, score: score2, reason };
1903
2459
  }).sort((a, b) => b.score - a.score || Number(b.resume.isDefault) - Number(a.resume.isDefault) || a.resume.createdAt.localeCompare(b.resume.createdAt));
1904
2460
  const best = scored[0] ?? null;
1905
2461
  return {
@@ -2239,7 +2795,7 @@ var profileMatch = {
2239
2795
  const cities = terms(facts.filter((f) => f.kind === "preference" && f.data.field === "城市").map((f) => f.data.value));
2240
2796
  const gaps = s.domain.db.prepare("select concept_id from user_gap where status = 'confirmed'").all().map((r) => r.concept_id);
2241
2797
  const me = userSide(facts, myLevel, gaps);
2242
- if (!me.evidence.length && !majors.length) throw new DomainError("PROFILE_EMPTY", "画像里还没有可用的方向、技能或项目,无法匹配");
2798
+ if (!me.evidence.length && !majors.length) throw new DomainError("PROFILE_EMPTY", "你的资料里还没有可用的方向、技能或项目,无法匹配");
2243
2799
  const distinct = new Set(me.evidence.map((e) => e.conceptId).filter((id) => concept(id)?.distinct));
2244
2800
  const majorText = /* @__PURE__ */ __name((c) => c.majorReq || (/专业(?:要求)?[::]\s*([^\n;;。]+)/.exec(c.jd)?.[1] ?? "") || (c.agg ? c.campaignMajor : ""), "majorText");
2245
2801
  const hits = [];
@@ -2316,12 +2872,12 @@ function expandHits(s, done) {
2316
2872
  const official = new Set(campaigns.filter((c) => live.has(c.id) && !isAggregate(c.source_kind)).map((c) => c.company_id));
2317
2873
  const name = db2.prepare("select name from company where id = ?");
2318
2874
  const twinHasJobs = /* @__PURE__ */ __name((id) => !!done && (units.get(unitKey(name.get(id).name)) ?? []).some((c) => c.id !== id && official.has(c.id)), "twinHasJobs");
2319
- for (const [companyId, score] of best) {
2875
+ for (const [companyId, score2] of best) {
2320
2876
  const rows = open.all(companyId);
2321
2877
  if (!rows.length || rows.some((r) => !isAggregate(r.source_kind)) || twinHasJobs(companyId)) continue;
2322
2878
  const e = entryOf(rows.flatMap((r) => [r.apply_url, r.notice_url]).filter(Boolean));
2323
2879
  if (e.url && done?.has(siteKeyOf(e.url))) continue;
2324
- hits.push({ companyId, score: score + (e.readable ? 1 : 0), reason: `只有汇总表信息;${e.label}` });
2880
+ hits.push({ companyId, score: score2 + (e.readable ? 1 : 0), reason: `只有汇总表信息;${e.label}` });
2325
2881
  }
2326
2882
  const groups = db2.prepare(`select id, industry from company where type = '央国企' and name not like '%(%' and name not like '%(%'
2327
2883
  and (name like '%集团' or name like '%集团有限公司' or (name like '中国%' and length(name) <= 6))`).all();
@@ -2459,7 +3015,7 @@ function pruneAutoSources(s) {
2459
3015
  const cared = /* @__PURE__ */ new Set();
2460
3016
  for (let i = 0; i < ids.length; i += 500) for (const r of s.domain.db.prepare(`select name from company where id in (${ids.slice(i, i + 500).map(() => "?").join(",")})`).all(...ids.slice(i, i + 500))) cared.add(r.name);
2461
3017
  const seen2 = /* @__PURE__ */ new Set();
2462
- const keep = s.config.sources.filter((c) => {
3018
+ const keep2 = s.config.sources.filter((c) => {
2463
3019
  const key = c.url ? siteKeyOf(c.url) : c.id;
2464
3020
  if (!c.auto || c.discover) {
2465
3021
  seen2.add(key);
@@ -2470,8 +3026,8 @@ function pruneAutoSources(s) {
2470
3026
  return true;
2471
3027
  });
2472
3028
  const before = s.config.sources.length;
2473
- if (keep.length !== before) saveSources(dataOf(s), keep);
2474
- return { before, after: keep.length };
3029
+ if (keep2.length !== before) saveSources(dataOf(s), keep2);
3030
+ return { before, after: keep2.length };
2475
3031
  }
2476
3032
  __name(pruneAutoSources, "pruneAutoSources");
2477
3033
 
@@ -4515,7 +5071,7 @@ async function browserRead(s, url, input, finish2) {
4515
5071
  });
4516
5072
  await opened.done();
4517
5073
  }, "done");
4518
- let keep = false;
5074
+ let keep2 = false;
4519
5075
  try {
4520
5076
  await page.goto(url, { waitUntil: "domcontentloaded", timeout: 2e4 }).catch(() => {
4521
5077
  });
@@ -4531,14 +5087,14 @@ async function browserRead(s, url, input, finish2) {
4531
5087
  const r = await extract(s, page, url, input, finish2, opened.page);
4532
5088
  page = r.pageOut;
4533
5089
  if (input.mode === "open" && r.result.status !== "blocked") {
4534
- keep = true;
5090
+ keep2 = true;
4535
5091
  const x = { id: ulid6(), page, done, origin, actions: (input.steps?.length ?? 0) + 1, createdAt: Date.now(), lastUsed: Date.now(), chain: Promise.resolve() };
4536
5092
  sessions.set(x.id, x);
4537
5093
  return { ...r.result, session: sessionInfo(x, "ready") };
4538
5094
  }
4539
5095
  return r.result;
4540
5096
  } finally {
4541
- if (!keep) await done();
5097
+ if (!keep2) await done();
4542
5098
  }
4543
5099
  }
4544
5100
  __name(browserRead, "browserRead");
@@ -4854,14 +5410,14 @@ function pickCandidates(rows, applied) {
4854
5410
  return out.sort((a, b) => b.prefScore - a.prefScore || String(a.closesAt ?? "9").localeCompare(String(b.closesAt ?? "9")));
4855
5411
  }
4856
5412
  __name(pickCandidates, "pickCandidates");
4857
- function exploreCandidates(s, q = {}) {
5413
+ function exploreCandidates(s, q2 = {}) {
4858
5414
  const db2 = s.domain.db;
4859
5415
  const since = s.config.season?.since ?? new Date(Date.now() - 180 * 864e5).toISOString().slice(0, 10);
4860
5416
  const applied = /* @__PURE__ */ new Map();
4861
5417
  for (const r of db2.prepare("select c.company_id id, count(*) n from application a join position p on p.id = a.position_id join campaign c on c.id = p.campaign_id where a.state != 'cancelled' and a.created_at >= ? group by c.company_id").all(since)) applied.set(r.id, r.n);
4862
5418
  const { items } = listPool(db2, { recommendedOnly: true, minTier: "可迁移", hideExpectationFail: true, limit: 5e3, sort: "match" });
4863
5419
  const all = pickCandidates(items, applied);
4864
- return { season: s.config.season?.key ?? "", since, total: all.length, items: all.slice(0, q.limit ?? 50) };
5420
+ return { season: s.config.season?.key ?? "", since, total: all.length, items: all.slice(0, q2.limit ?? 50) };
4865
5421
  }
4866
5422
  __name(exploreCandidates, "exploreCandidates");
4867
5423
 
@@ -5332,7 +5888,7 @@ function login(s) {
5332
5888
  return manager ??= createOAuthLoginManager({ models: s.llm.models });
5333
5889
  }
5334
5890
  __name(login, "login");
5335
- var remembered = /* @__PURE__ */ __name((s, provider) => (YAML2.parse(readFileSync13(configFile(s.config.vault), "utf8"))?.llm?.selections ?? {})[provider] ?? "", "remembered");
5891
+ var remembered2 = /* @__PURE__ */ __name((s, provider) => (YAML2.parse(readFileSync13(configFile(s.config.vault), "utf8"))?.llm?.selections ?? {})[provider] ?? "", "remembered");
5336
5892
  function persist(s, provider, model) {
5337
5893
  updateConfig(s.config.vault, (doc) => {
5338
5894
  doc.llm = { ...doc.llm ?? {}, provider, model, selections: { ...doc.llm?.selections ?? {}, ...model ? { [provider]: model } : {} } };
@@ -5406,7 +5962,7 @@ async function modelProvider(s, providerId, authType) {
5406
5962
  if (phase === "running" || phase === "waiting_for_input") lm.cancel();
5407
5963
  chosenAuth = authType;
5408
5964
  if (providerId !== s.config.llm.provider) {
5409
- const list2 = listProviderModels(s.llm.models, providerId), was = remembered(s, providerId);
5965
+ const list2 = listProviderModels(s.llm.models, providerId), was = remembered2(s, providerId);
5410
5966
  persist(s, providerId, list2.some((m) => m.id === was) ? was : list2[0]?.id ?? "");
5411
5967
  }
5412
5968
  return modelStatus(s);
@@ -5471,13 +6027,13 @@ __name(tagCompany, "tagCompany");
5471
6027
  import { join as join12 } from "node:path";
5472
6028
 
5473
6029
  // apps/daemon/src/service/opportunity-gaps.ts
5474
- function listGaps(s, q = {}) {
6030
+ function listGaps(s, q2 = {}) {
5475
6031
  const all = s.domain.db.prepare(`select a.position_id, a.position_id id, p.campaign_id, p.closes_at, a.json, p.title, co.name company, c.company_id from position_assessment a
5476
6032
  join position p on p.id = a.position_id join campaign c on c.id = p.campaign_id join company co on co.id = c.company_id
5477
6033
  where p.status != 'closed'`).all();
5478
6034
  const expired2 = expiredMany(s.domain.db, all);
5479
6035
  const rows = all.filter((r) => !expired2.get(r.id));
5480
- const minA = q.minA ?? 30;
6036
+ const minA = q2.minA ?? 30;
5481
6037
  const acc = /* @__PURE__ */ new Map();
5482
6038
  let related = 0;
5483
6039
  for (const r of rows) {
@@ -5833,7 +6389,8 @@ var handlers = {
5833
6389
  s.onDataChanged?.();
5834
6390
  return r;
5835
6391
  }, "import.csv"),
5836
- "listing.capture": /* @__PURE__ */ __name(async (i, _a, s) => {
6392
+ "listing.capture": /* @__PURE__ */ __name(async (i, a, s) => {
6393
+ if (a.kind === "agent" && providerOf(i.url).provider === "boss") throw new ToolError("USE_BOOKMARK", "BOSS 直聘的岗位请用户自己在浏览器里打开,用捕获书签收;BOSS 的用户协议不允许程序代为读取");
5837
6394
  const pull2 = sourceFor(i.url);
5838
6395
  if (pull2?.fetch) {
5839
6396
  const tenantUrl = pull2.id === "moka" ? i.url.split("#")[0] : i.url;
@@ -5886,7 +6443,7 @@ var handlers = {
5886
6443
  s.onDataChanged?.();
5887
6444
  return { id: i.id, name: cfg.name ?? "", droppedCampaigns };
5888
6445
  }, "source.remove"),
5889
- "research.company": /* @__PURE__ */ __name((i, _a, s) => startResearch(s, i.companyName, i.question), "research.company"),
6446
+ "research.company": /* @__PURE__ */ __name((i, _a, s) => i.question === "就业评估" ? startEmployerReport(s, i.companyName) : startResearch(s, i.companyName, i.question), "research.company"),
5890
6447
  "research.topic": /* @__PURE__ */ __name(async (i, _a, s) => {
5891
6448
  try {
5892
6449
  return await startTopicResearch(s, i);
@@ -5898,6 +6455,7 @@ var handlers = {
5898
6455
  "industry.reports": /* @__PURE__ */ __name((i, _a, s) => dataTool("industry.reports", i, s), "industry.reports"),
5899
6456
  "industry.companies": /* @__PURE__ */ __name((i, _a, s) => dataTool("industry.companies", i, s), "industry.companies"),
5900
6457
  "company.financials": /* @__PURE__ */ __name((i, _a, s) => dataTool("company.financials", i, s), "company.financials"),
6458
+ "employer.compare": /* @__PURE__ */ __name((i, _a, s) => compare(s.domain.db, i), "employer.compare"),
5901
6459
  "labor.us": /* @__PURE__ */ __name((i, _a, s) => dataTool("labor.us", i, s), "labor.us"),
5902
6460
  "research.report": /* @__PURE__ */ __name((i, _a, s) => {
5903
6461
  const r = getTopic(s, i.runId);
@@ -6080,9 +6638,9 @@ var handlers = {
6080
6638
  "company.official": /* @__PURE__ */ __name((i, _a, s) => ({ links: officialLinks(s, i.companyId) }), "company.official"),
6081
6639
  "company.syncOfficial": /* @__PURE__ */ __name((i, _a, s) => syncOfficial(s, i.companyId), "company.syncOfficial"),
6082
6640
  "company.search": /* @__PURE__ */ __name((i, _a, s) => searchCompanies(s.domain.db, i.query, i.limit ?? 10), "company.search"),
6083
- "company.intel": /* @__PURE__ */ __name((i, _a, s) => getIntel(s, i.companyId), "company.intel"),
6641
+ "company.intel": /* @__PURE__ */ __name((i, _a, s) => readIntel(s, i.companyId), "company.intel"),
6084
6642
  "company.intelDetail": /* @__PURE__ */ __name((i, _a, s) => getIntelDetail(s, i.companyId, i.kind), "company.intelDetail"),
6085
- "company.intelRefresh": /* @__PURE__ */ __name((i, _a, s) => refreshIntel(s, i.companyId), "company.intelRefresh"),
6643
+ "company.intelRefresh": /* @__PURE__ */ __name((i, _a, s) => refreshIntel(s, i.companyId, { force: i.force !== false }), "company.intelRefresh"),
6086
6644
  "intel.smeImport": /* @__PURE__ */ __name((i, _a, s) => importSme(s.domain.db, s.config.vault, i.path), "intel.smeImport"),
6087
6645
  "channel.list": /* @__PURE__ */ __name((_i, _a, s) => ({ items: listChannels(s) }), "channel.list"),
6088
6646
  "intel.smeFromUrl": /* @__PURE__ */ __name((i, _a, s) => loadSources(s.domain.db, [{ url: i.url, level: i.level, batch: i.batch, region: i.region, published_at: i.publishedAt }]), "intel.smeFromUrl"),
@@ -6114,6 +6672,13 @@ var handlers = {
6114
6672
  setAgentConfig(s, { backend: i.backend });
6115
6673
  return { backend: i.backend };
6116
6674
  }, "agent.backend"),
6675
+ // a model for Zhiro's runs of Claude Code / Codex; "" = follow the harness's own setting. Their settings are not touched.
6676
+ "agent.model": /* @__PURE__ */ __name((i, a, s) => {
6677
+ humanOnly(a);
6678
+ setAgentConfig(s, { models: { ...s.config.agent?.models ?? {}, [i.backend]: i.model } });
6679
+ chooseModel(i.backend, i.model);
6680
+ return { backend: i.backend, model: i.model, using: followedModel(i.backend) };
6681
+ }, "agent.model"),
6117
6682
  "agent.harnessConfirm": /* @__PURE__ */ __name((i, a, s) => {
6118
6683
  humanOnly(a);
6119
6684
  setAgentConfig(s, { harnessConfirm: i.where });
@@ -6168,7 +6733,7 @@ var handlers = {
6168
6733
  llm: { ...s.config.llm, ...s.llm ? await s.llm.status() : {} },
6169
6734
  worker: workerStatus(),
6170
6735
  // ADR-046 / 047: who answers the drawer, what is installed; where the person's own harness confirms
6171
- chat: { backend: backendOf(s), backends: s.chat?.status() ?? {}, harnessConfirm: harnessConfirm(s), agentDir: join12(s.config.vault, "agent"), models: { claude: followedModel("claude"), codex: followedModel("codex"), refused: { claude: modelRefused("claude"), codex: modelRefused("codex") } } },
6736
+ chat: { backend: backendOf(s), backends: s.chat?.status() ?? {}, harnessConfirm: harnessConfirm(s), agentDir: join12(s.config.vault, "agent"), models: { claude: followedModel("claude"), codex: followedModel("codex"), refused: { claude: modelRefused("claude"), codex: modelRefused("codex") }, own: { claude: harnessModel("claude"), codex: harnessModel("codex") }, chosen: s.config.agent?.models ?? {}, choices: { claude: harnessModels("claude"), codex: harnessModels("codex") } } },
6172
6737
  background: { useModel: backgroundOnModel(s), doneBy: describeBackground(s) },
6173
6738
  applyReference: s.config.apply?.referenceDoc ?? "",
6174
6739
  configFile: s.config.vault + "\\config\\jobos.yaml"
@@ -6232,7 +6797,7 @@ function executeDirect(name, input, actor, s) {
6232
6797
  return h(input, actor, s);
6233
6798
  }
6234
6799
  __name(executeDirect, "executeDirect");
6235
- var HUMAN_ONLY = /* @__PURE__ */ new Set(["gap.confirm", "model.login", "model.answer", "model.cancel", "model.provider", "model.select", "model.refresh", "inbox.connect", "inbox.disconnect", "history.undo", "agent.mode", "agent.backend", "agent.harnessConfirm", "agent.background", "data.export", "data.move"]);
6800
+ var HUMAN_ONLY = /* @__PURE__ */ new Set(["agent.model", "gap.confirm", "model.login", "model.answer", "model.cancel", "model.provider", "model.select", "model.refresh", "inbox.connect", "inbox.disconnect", "history.undo", "agent.mode", "agent.backend", "agent.harnessConfirm", "agent.background", "data.export", "data.move"]);
6236
6801
  var callableBy = /* @__PURE__ */ __name((actor, s) => {
6237
6802
  const harness = !!s && mayConfirmInHarness(actor, s);
6238
6803
  return listContracts().filter((c) => actor.kind === "human" || harness && (c.name === "proposal.decide" || c.name === "history.undo") || !HUMAN_ONLY.has(c.name) && c.name !== "proposal.decide");
@@ -6809,7 +7374,7 @@ ${dl}`, `待决提案:${today.pendingProposals};待办下一步:${today.ne
6809
7374
  "岗位的 match.reason 以 待补资料 开头、或岗位名只是类别(「机械工程」「技术类」)、或 position_get 没有岗位描述时,不要据此判断合不合适:先 lead_resolve({companyIds:[…]}) 让系统读公告页补出具体岗位(几秒到一分钟),再用 pool_list 看补出来的岗位;仍补不出就如实说「这家只有类别,入口是 X,需要手动查看」。判断合不合适看 match.reason 里的档位与待核实项,细看用 position_get。",
6810
7375
  "一条消息里有 3 题以上互不依赖的调研任务(各自要读不同网页):用 research_dispatch 一次派出全部题目(itemId、题目原文、该题的输出格式),等结果回来后按用户要求的格式汇总,status 不是 answered 的题照实写明原因;不要自己逐题读页。",
6811
7376
  "长调研(要读很多页才能回答):每拿到一题就立刻 research_checkpoint 记下该题的最终内容和链接——较早的页面内容会被省略,没记的就没有了,收尾时没记录的题按未完成处理;先查岗位库(pool_list / company_search / position_get:8 千家公司的官方入口、国能等站的全量岗位都在库里),库里没有再 page_read;同一站点要连做几步(选单位 → 点岗位 → 下一页)用 page_read 的 mode=open + continue,不要每次从首页重来;读列表页后按 links 读详情;不同站点、不同题目的 page_read 在同一条消息里一起发起(它们会并行执行,一条一条发要慢好几倍);一个站点读不到(needsLogin / blocked)就标 blocked 换下一题,不要反复重试;全部题目记完调用 research_finish,再按用户要求的格式写出完整答案。",
6812
- "问一家公司的经营状况(收益、研发、员工数、分红、专精特新、IPO、扩产、投资)先 company_intel;stale 为 true 或没有数据就 company_intelRefresh 再看;非上市公司只有名录匹配,其余用 research_company 派联网调研(会出确认卡)。回答时带上每类数据的更新日期。",
7377
+ "问一家公司的经营状况(收益、研发、员工数、分红、专精特新、IPO、扩产、投资)先 company_intel;过期的类会在后台自动更新(refreshing=true 时稍后再读);从没生成过(listing 为空)才调 company_intelRefresh;非上市公司只有名录匹配,其余用 research_company 派联网调研(会出确认卡)。回答时带上每类数据的更新日期。",
6813
7378
  "问一个行业或方向(规模、哪些公司、谁在赚钱、薪资):直接用数据工具取数再答——industry_companies(这个主题在股市里的上市公司,候选名单)、company_financials(按公司名或代码查任何上市公司的营收 / 净利 / 研发 / 员工数,不必在岗位库里)、industry_reports(券商最近的研报)、labor_us(美国某职业的就业人数与年薪)。回答里写明数字的财年、币种和来源。要一份完整的报告再用 research_topic(会出确认卡)。",
6814
7379
  "工具返回里如果有 changedByUser,是用户在 Zhiro 窗口里自你上次调用以来改过的东西(一行一处):以它为准更新你记着的情况,必要时重新读,不要把旧的说法再告诉用户。",
6815
7380
  "批量删除、批量结束申请这类影响多条数据的操作,先说清会影响哪些、请用户确认,再调用工具。授权码、密码、模型登录只能由用户在界面里操作,不要在对话里索要或转交。",
@@ -7329,6 +7894,7 @@ try {
7329
7894
  }
7330
7895
  throw e;
7331
7896
  }
7897
+ for (const n of ["claude", "codex"]) chooseModel(n, config.agent?.models?.[n] ?? "");
7332
7898
  var lock;
7333
7899
  try {
7334
7900
  lock = await lockVault(config.vault, "daemon");