@mazjq/zhiro 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +228 -2
- package/README.zh-CN.md +188 -0
- package/agent-skills/zhiro/SKILL.md +89 -0
- package/bin/zhiro.cjs +21 -0
- package/dist/acp-bridge.js +87 -0
- package/dist/chunk-2SSYR7VW.js +558 -0
- package/dist/chunk-2XJ45GUM.js +3795 -0
- package/dist/chunk-4EPNDXRS.js +542 -0
- package/dist/chunk-4JJEOOW2.js +107 -0
- package/dist/chunk-4UWMDKMH.js +2605 -0
- package/dist/chunk-7IV2NG2D.js +328 -0
- package/dist/chunk-A722HITH.js +680 -0
- package/dist/chunk-GRZ4VMSO.js +180 -0
- package/dist/chunk-H3EFVYGD.js +6 -0
- package/dist/chunk-OOHBM3BT.js +6 -0
- package/dist/chunk-VOUILWMI.js +848 -0
- package/dist/chunk-ZKD2BKN3.js +1158 -0
- package/dist/chunk-ZUUYRVX6.js +44 -0
- package/dist/cli-LUWVZ5NG.js +131 -0
- package/dist/login-W5QYFWTY.js +75 -0
- package/dist/main-FTWFKWZH.js +8029 -0
- package/dist/research-data.js +46 -0
- package/dist/stdio-FZZLZI4P.js +32 -0
- package/dist/vault-JUTREQJK.js +117 -0
- package/dist/zhiro.js +62 -0
- package/package.json +57 -4
- package/ui/assets/core-CwxXejkd.js +1 -0
- package/ui/assets/index-B7GrU8uc.js +39 -0
- package/ui/assets/index-Dt8b-A97.css +2 -0
- package/ui/index.html +15 -0
- package/ui/tauri.svg +6 -0
- package/ui/vite.svg +1 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name
|
|
3
|
+
} from "./chunk-2SSYR7VW.js";
|
|
4
|
+
|
|
5
|
+
// apps/daemon/src/db.ts
|
|
6
|
+
import { DatabaseSync } from "node:sqlite";
|
|
7
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
8
|
+
import { dirname } from "node:path";
|
|
9
|
+
var MIGRATIONS = [
|
|
10
|
+
`
|
|
11
|
+
create table meta (key text primary key, value text not null);
|
|
12
|
+
create table company (
|
|
13
|
+
id text primary key, name text not null unique, aliases text not null default '[]',
|
|
14
|
+
type text not null default '', industry text not null default '', created_at text not null
|
|
15
|
+
);
|
|
16
|
+
create table campaign (
|
|
17
|
+
id text primary key, company_id text not null references company(id),
|
|
18
|
+
external_key text not null unique,
|
|
19
|
+
kind text not null default '校招', cohort text not null default '', batch text not null default '',
|
|
20
|
+
cities text not null default '[]', degree_req text not null default '', major_req text not null default '',
|
|
21
|
+
opens_at text, closes_at text, deadline_kind text not null default 'unknown', deadline_text text not null default '',
|
|
22
|
+
apply_url text not null default '', notice_url text not null default '',
|
|
23
|
+
source_grade text not null default '聚合', source_kind text not null default '',
|
|
24
|
+
has_written_test text not null default '', notes text not null default '',
|
|
25
|
+
last_verified_at text, created_at text not null, updated_at text not null
|
|
26
|
+
);
|
|
27
|
+
create index campaign_closes on campaign(closes_at);
|
|
28
|
+
create table position (
|
|
29
|
+
id text primary key, campaign_id text not null references campaign(id),
|
|
30
|
+
title text not null, jd_text text not null default '', cities text not null default '[]',
|
|
31
|
+
degree_req text not null default '', status text not null default 'open',
|
|
32
|
+
direction_tags text not null default '[]', last_seen_at text,
|
|
33
|
+
watch_priority integer, watch_note text not null default '',
|
|
34
|
+
created_at text not null, updated_at text not null,
|
|
35
|
+
unique(campaign_id, title)
|
|
36
|
+
);
|
|
37
|
+
create table source_listing (
|
|
38
|
+
id text primary key, position_id text references position(id), campaign_id text references campaign(id),
|
|
39
|
+
provider text not null, tenant text not null default '', external_id text not null default '',
|
|
40
|
+
url text not null default '', publisher text not null default '', grade text not null default '聚合',
|
|
41
|
+
is_original integer not null default 0, extracted_json text not null default '{}', confidence real not null default 0,
|
|
42
|
+
content_path text not null default '', fetched_at text not null
|
|
43
|
+
);
|
|
44
|
+
create table application (
|
|
45
|
+
id text primary key, position_id text not null references position(id), campaign_id text not null references campaign(id),
|
|
46
|
+
state text not null default 'draft', close_reason text not null default '', channel text not null default '',
|
|
47
|
+
external_app_id text not null default '', referral_code text not null default '',
|
|
48
|
+
submitted_at text, next_action text not null default '', next_action_at text,
|
|
49
|
+
created_at text not null, updated_at text not null,
|
|
50
|
+
unique(position_id)
|
|
51
|
+
);
|
|
52
|
+
create table application_event (
|
|
53
|
+
id text primary key, application_id text not null references application(id),
|
|
54
|
+
at text not null, kind text not null, detail text not null default '', source text not null default 'manual'
|
|
55
|
+
);
|
|
56
|
+
create table stage (
|
|
57
|
+
id text primary key, application_id text not null references application(id),
|
|
58
|
+
kind text not null, scheduled_at text, tz text not null default 'Asia/Shanghai', location text not null default '',
|
|
59
|
+
link text not null default '', result text not null default '', notes text not null default '', created_at text not null
|
|
60
|
+
);
|
|
61
|
+
create table proposal (
|
|
62
|
+
id text primary key, kind text not null, payload_json text not null, payload_hash text not null,
|
|
63
|
+
created_by text not null, reason text not null default '', decision text not null default 'pending',
|
|
64
|
+
expires_at text not null, decided_at text, result_json text, created_at text not null
|
|
65
|
+
);
|
|
66
|
+
create table fact (
|
|
67
|
+
id text primary key, key text not null, statement text not null, grade text not null default '待验证',
|
|
68
|
+
source text not null default '', created_at text not null
|
|
69
|
+
);
|
|
70
|
+
create table import_row (
|
|
71
|
+
row_hash text primary key, source text not null, raw_json text not null, campaign_id text, imported_at text not null
|
|
72
|
+
);
|
|
73
|
+
create table workflow (
|
|
74
|
+
id text primary key, kind text not null, subject_id text not null, state text not null default 'running',
|
|
75
|
+
current_step text not null default '', idempotency_key text not null unique,
|
|
76
|
+
checkpoint_json text not null default '{}', next_run_at text, attempts integer not null default 0,
|
|
77
|
+
lease_until text, last_error text not null default '', created_at text not null, updated_at text not null
|
|
78
|
+
);
|
|
79
|
+
create index workflow_next on workflow(state, next_run_at);
|
|
80
|
+
create table audit (
|
|
81
|
+
id text primary key, at text not null, actor text not null, tool text not null,
|
|
82
|
+
input_json text not null, ok integer not null, detail text not null default ''
|
|
83
|
+
);
|
|
84
|
+
create table capture (
|
|
85
|
+
id text primary key, url text not null, title text not null default '', text_path text not null,
|
|
86
|
+
publisher text not null default '', created_at text not null
|
|
87
|
+
);
|
|
88
|
+
`,
|
|
89
|
+
`
|
|
90
|
+
create table scan_run (
|
|
91
|
+
id text primary key, source text not null, config_id text not null default '',
|
|
92
|
+
started_at text not null, finished_at text, complete integer not null default 1,
|
|
93
|
+
found integer not null default 0, new_positions integer not null default 0,
|
|
94
|
+
error text not null default '', snapshot_path text not null default ''
|
|
95
|
+
);
|
|
96
|
+
create index scan_run_source on scan_run(source, started_at);
|
|
97
|
+
`,
|
|
98
|
+
`
|
|
99
|
+
create table agent_run (
|
|
100
|
+
id text primary key, runner text not null, mode text not null, subject_type text not null default '', subject_id text not null default '',
|
|
101
|
+
subject_label text not null default '', run_dir text not null, session_id text not null default '', status text not null default 'queued',
|
|
102
|
+
output_path text not null default '', summary text not null default '', error text not null default '',
|
|
103
|
+
cost_usd real not null default 0, started_at text not null, finished_at text
|
|
104
|
+
);
|
|
105
|
+
create index agent_run_subject on agent_run(subject_type, subject_id, started_at);
|
|
106
|
+
create table evidence (
|
|
107
|
+
id text primary key, subject_type text not null, subject_id text not null, grade text not null,
|
|
108
|
+
url text not null default '', title text not null default '', quote text not null default '',
|
|
109
|
+
fetched_at text not null, report_path text not null default '', run_id text not null default ''
|
|
110
|
+
);
|
|
111
|
+
create index evidence_subject on evidence(subject_type, subject_id);
|
|
112
|
+
`,
|
|
113
|
+
// v4 (ADR-014/015/016): structured profile, company name key, recommendations. Old `fact` was never written.
|
|
114
|
+
`
|
|
115
|
+
alter table company add column norm text not null default '';
|
|
116
|
+
create index company_norm on company(norm);
|
|
117
|
+
drop table fact;
|
|
118
|
+
create table document (
|
|
119
|
+
id text primary key, path text not null, sha256 text not null unique, kind text not null default '',
|
|
120
|
+
text_path text not null default '', egress text not null default 'allow', added_at text not null
|
|
121
|
+
);
|
|
122
|
+
create table fact (
|
|
123
|
+
id text primary key, kind text not null, data_json text not null default '{}', statement text not null default '',
|
|
124
|
+
grade text not null default '待验证', dedup_key text not null, created_at text not null, updated_at text not null,
|
|
125
|
+
unique(kind, dedup_key)
|
|
126
|
+
);
|
|
127
|
+
create table fact_source (
|
|
128
|
+
id text primary key, fact_id text not null references fact(id) on delete cascade,
|
|
129
|
+
document_id text references document(id), quote text not null default '', locator text not null default '', added_at text not null
|
|
130
|
+
);
|
|
131
|
+
create index fact_source_fact on fact_source(fact_id);
|
|
132
|
+
create table recommendation (
|
|
133
|
+
id text primary key, company_id text not null references company(id), position_id text not null default '',
|
|
134
|
+
mode text not null, tag text not null, score real not null default 0, reason text not null default '',
|
|
135
|
+
run_id text not null default '', created_at text not null,
|
|
136
|
+
unique(company_id, position_id, mode, tag)
|
|
137
|
+
);
|
|
138
|
+
create index recommendation_company on recommendation(company_id);
|
|
139
|
+
`,
|
|
140
|
+
// v5: ingest looks listings up by (provider, external_id); unindexed it was O(n²) and froze the daemon on a 57k-row sheet
|
|
141
|
+
`
|
|
142
|
+
create index source_listing_ext on source_listing(provider, external_id);
|
|
143
|
+
`,
|
|
144
|
+
// v6: which resume went out with an application (free text now; a resume-library id later)
|
|
145
|
+
`
|
|
146
|
+
alter table application add column resume_label text not null default '';
|
|
147
|
+
`,
|
|
148
|
+
// v7: user-defined 搜索模式 (AND / OR / NOT keyword searches kept by name)
|
|
149
|
+
`
|
|
150
|
+
create table recommend_preset (
|
|
151
|
+
id text primary key, name text not null unique, params_json text not null default '{}', created_at text not null, updated_at text not null
|
|
152
|
+
);
|
|
153
|
+
`,
|
|
154
|
+
// v8: documents are extracted in the background, one at a time
|
|
155
|
+
`
|
|
156
|
+
alter table document add column status text not null default 'done';
|
|
157
|
+
alter table document add column facts integer not null default 0;
|
|
158
|
+
alter table document add column error text not null default '';
|
|
159
|
+
`,
|
|
160
|
+
// v9: companyKey had a broken whitespace rule — recompute every key (Domain refills and folds the twins);
|
|
161
|
+
// documents report which step of extraction they are in
|
|
162
|
+
`
|
|
163
|
+
update company set norm = '';
|
|
164
|
+
alter table document add column stage text not null default '';
|
|
165
|
+
alter table document add column started_at text;
|
|
166
|
+
`,
|
|
167
|
+
// v10: 最近删除 — the last few deleted fact cards, restorable
|
|
168
|
+
`
|
|
169
|
+
create table fact_trash (id text primary key, fact_json text not null, sources_json text not null default '[]', deleted_at text not null);
|
|
170
|
+
`,
|
|
171
|
+
// v11: recruiting mail (only mail that passed the recruiting filter is ever stored); body text lives in vault/inbox
|
|
172
|
+
`
|
|
173
|
+
create table message (
|
|
174
|
+
id text primary key, account text not null, message_id text not null unique, sender text not null default '', subject text not null default '',
|
|
175
|
+
received_at text not null, body_path text not null default '', category text not null default '其他', company text not null default '', position text not null default '',
|
|
176
|
+
scheduled_at text, link text not null default '', summary text not null default '', confidence real not null default 0,
|
|
177
|
+
application_id text references application(id), proposal_id text, status text not null default 'new', created_at text not null
|
|
178
|
+
);
|
|
179
|
+
create index message_received on message(received_at);
|
|
180
|
+
`,
|
|
181
|
+
// v12: inbox hardening — content-level dedup, interview rounds, junk-folder origin, why nothing was proposed; and a
|
|
182
|
+
// subject-only log of mail the recruiting filter skipped, so a miss is visible and recoverable
|
|
183
|
+
`
|
|
184
|
+
alter table message add column content_key text not null default '';
|
|
185
|
+
alter table message add column round text not null default '';
|
|
186
|
+
alter table message add column note text not null default '';
|
|
187
|
+
alter table message add column folder text not null default '';
|
|
188
|
+
create index message_content on message(content_key);
|
|
189
|
+
create table mail_skipped (
|
|
190
|
+
account text not null, message_id text not null, sender text not null default '', subject text not null default '',
|
|
191
|
+
received_at text not null, folder text not null default '', primary key (account, message_id)
|
|
192
|
+
);
|
|
193
|
+
`,
|
|
194
|
+
// v13: 简历库 — files the user wrote, tagged with the 公司类型 and 岗位关键词 each was written for
|
|
195
|
+
`
|
|
196
|
+
create table resume (
|
|
197
|
+
id text primary key, name text not null unique, file_name text not null default '', file_path text not null default '', text_path text not null default '',
|
|
198
|
+
company_types text not null default '[]', role_keywords text not null default '[]', note text not null default '', is_default integer not null default 0, created_at text not null
|
|
199
|
+
);
|
|
200
|
+
`,
|
|
201
|
+
// v14: 不投了 before submitting is "cancelled", not "closed" (ADR-023); rows closed without ever being submitted were exactly that
|
|
202
|
+
`
|
|
203
|
+
update application set state = 'cancelled' where state = 'closed' and submitted_at is null;
|
|
204
|
+
`,
|
|
205
|
+
// v15: the order the user dragged the cards into (画像页); 0 = never moved, falls back to creation order
|
|
206
|
+
`
|
|
207
|
+
alter table fact add column sort integer not null default 0;
|
|
208
|
+
`,
|
|
209
|
+
// v16: tokens per model call and what it was for (配置 → 模型用量)
|
|
210
|
+
`
|
|
211
|
+
create table llm_usage (
|
|
212
|
+
id text primary key, at text not null, purpose text not null, ref text not null default '', provider text not null, model text not null,
|
|
213
|
+
input integer not null default 0, output integer not null default 0, cache_read integer not null default 0, total integer not null default 0, cost real not null default 0
|
|
214
|
+
);
|
|
215
|
+
create index llm_usage_at on llm_usage(at);
|
|
216
|
+
`,
|
|
217
|
+
// v17: ADR-025 — writes the assistant did without asking, each with the inverse call an undo replays
|
|
218
|
+
`
|
|
219
|
+
create table agent_action (
|
|
220
|
+
id text primary key, at text not null, actor text not null, tool text not null, input_json text not null,
|
|
221
|
+
summary text not null default '', undo_json text, undone_at text
|
|
222
|
+
);
|
|
223
|
+
`,
|
|
224
|
+
// v18: the assistant's transcript survives a daemon restart (one conversation; provider/model it was had with)
|
|
225
|
+
`
|
|
226
|
+
create table chat (
|
|
227
|
+
id text primary key, provider text not null, model text not null, session_id text not null,
|
|
228
|
+
messages_json text not null default '[]', updated_at text not null
|
|
229
|
+
);
|
|
230
|
+
`,
|
|
231
|
+
// v19: the first 飞书招聘 sync (2026-09-22) pulled a tenant's 社招 portal into 校招 campaigns (bambulab/experienced, 387
|
|
232
|
+
// jobs); the source now skips 社招 posts, this removes what it had ingested. Watched or applied positions stay.
|
|
233
|
+
`
|
|
234
|
+
create index if not exists source_listing_position on source_listing(position_id);
|
|
235
|
+
create temp table gone_social as
|
|
236
|
+
select p.id from position p
|
|
237
|
+
where p.id in (select distinct position_id from source_listing where provider = 'feishu-jobs' and extracted_json like '%"kind":"社招"%' and position_id is not null)
|
|
238
|
+
and p.watch_priority is null
|
|
239
|
+
and p.id not in (select position_id from application);
|
|
240
|
+
delete from source_listing where provider = 'feishu-jobs' and extracted_json like '%"kind":"社招"%';
|
|
241
|
+
delete from gone_social where id in (select position_id from source_listing where position_id is not null);
|
|
242
|
+
delete from position where id in (select id from gone_social);
|
|
243
|
+
drop table gone_social;
|
|
244
|
+
`,
|
|
245
|
+
// v20: 企业情况 (ADR-029) — one row per company per kind of fact, each with its own fetched_at so staleness is visible;
|
|
246
|
+
// and the 专精特新 roster loaded from a CSV the user or a worker maintains
|
|
247
|
+
`
|
|
248
|
+
create table company_intel (
|
|
249
|
+
company_id text not null references company(id), kind text not null, data_json text not null default '{}',
|
|
250
|
+
source text not null default '', fetched_at text not null, primary key(company_id, kind)
|
|
251
|
+
);
|
|
252
|
+
create table sme_list (
|
|
253
|
+
name text primary key, level text not null, batch text not null default '', region text not null default '',
|
|
254
|
+
source_url text not null default '', published_at text not null default '', updated_at text not null
|
|
255
|
+
);
|
|
256
|
+
`,
|
|
257
|
+
// v21: a company can be on several 专精特新 lists (小巨人 + 重点小巨人 + 省级) — one row per (name, level)
|
|
258
|
+
`
|
|
259
|
+
drop table sme_list;
|
|
260
|
+
create table sme_list (
|
|
261
|
+
name text not null, level text not null, batch text not null default '', region text not null default '',
|
|
262
|
+
source_url text not null default '', published_at text not null default '', updated_at text not null,
|
|
263
|
+
primary key(name, level)
|
|
264
|
+
);
|
|
265
|
+
`,
|
|
266
|
+
// v22: model work an external agent answers (ADR-032): kind 'complete' = one model call keyed by its prompt hash,
|
|
267
|
+
// kind 'run' = a research worker run (id = agent_run.id); pending → answered → used
|
|
268
|
+
`
|
|
269
|
+
create table model_job (
|
|
270
|
+
id text primary key, kind text not null default 'complete', hash text unique, purpose text not null default '', ref text not null default '',
|
|
271
|
+
system text not null default '', user_text text not null default '', status text not null default 'pending', answer text,
|
|
272
|
+
created_at text not null, answered_at text
|
|
273
|
+
);
|
|
274
|
+
create index model_job_status on model_job(status);
|
|
275
|
+
`,
|
|
276
|
+
// v23: 岗位适配度 (ADR-034). A post's own major requirement (it was only kept per campaign — the first post's), an index
|
|
277
|
+
// the per-company lookups were missing, and the model's reading of a post against the profile (layer 2, on demand).
|
|
278
|
+
`
|
|
279
|
+
alter table position add column major_req text not null default '';
|
|
280
|
+
create index if not exists campaign_company on campaign(company_id);
|
|
281
|
+
create table position_fit (
|
|
282
|
+
position_id text primary key references position(id), score integer not null, verdict text not null default '',
|
|
283
|
+
meets text not null default '[]', gaps text not null default '[]', model text not null default '', profile_hash text not null default '',
|
|
284
|
+
created_at text not null
|
|
285
|
+
);
|
|
286
|
+
`,
|
|
287
|
+
// v24: 岗位适配度 v2 (ADR-035) — every requirement of a post judged against the profile (supported / transferable /
|
|
288
|
+
// unknown / gap), kept per post so the 机会缺口 ledger aggregates them; user-confirmed gaps are the only negatives.
|
|
289
|
+
`
|
|
290
|
+
create table position_assessment (
|
|
291
|
+
position_id text primary key references position(id), json text not null, profile_hash text not null default '', rules_version text not null default '',
|
|
292
|
+
created_at text not null
|
|
293
|
+
);
|
|
294
|
+
create table user_gap (
|
|
295
|
+
concept_id text primary key, status text not null default 'confirmed', note text not null default '', created_at text not null
|
|
296
|
+
);
|
|
297
|
+
`,
|
|
298
|
+
// v25: what a person (or their agent, after research) says about a deadline lives beside the source's value, so the
|
|
299
|
+
// next pull of that source cannot put the sheet's date back (ADR-035); a post's own deadline when its source states one
|
|
300
|
+
`
|
|
301
|
+
alter table campaign add column closes_at_override text;
|
|
302
|
+
alter table campaign add column override_note text not null default '';
|
|
303
|
+
alter table campaign add column overridden_at text;
|
|
304
|
+
alter table position add column closes_at text;
|
|
305
|
+
`,
|
|
306
|
+
// v26: 期望卡 (ADR-036) — one 期望 (preference) card per field, the values on it are OR; cards that shared a field merge
|
|
307
|
+
// into the oldest one, and the dedup key becomes the field alone
|
|
308
|
+
`
|
|
309
|
+
update fact set data_json = json_set(data_json, '$.value', (select group_concat(json_extract(f2.data_json, '$.value'), '、') from fact f2 where f2.kind = 'preference' and json_extract(f2.data_json, '$.field') = json_extract(fact.data_json, '$.field')))
|
|
310
|
+
where kind = 'preference' and created_at = (select min(f3.created_at) from fact f3 where f3.kind = 'preference' and json_extract(f3.data_json, '$.field') = json_extract(fact.data_json, '$.field'));
|
|
311
|
+
delete from fact where kind = 'preference' and created_at <> (select min(f3.created_at) from fact f3 where f3.kind = 'preference' and json_extract(f3.data_json, '$.field') = json_extract(fact.data_json, '$.field'));
|
|
312
|
+
update fact set dedup_key = coalesce(json_extract(data_json, '$.field'), dedup_key), statement = coalesce(json_extract(data_json, '$.field'), '') || ' · ' || coalesce(json_extract(data_json, '$.value'), '') where kind = 'preference';
|
|
313
|
+
`,
|
|
314
|
+
// v27: 读页 (ADR-038) — every page the assistant or a job read, for the day cache, the browser budget and the audit trail
|
|
315
|
+
`
|
|
316
|
+
create table page_read (
|
|
317
|
+
id text primary key, url text not null, final_url text not null default '', status text not null, via text not null,
|
|
318
|
+
chars integer not null default 0, text_path text not null default '', created_at text not null
|
|
319
|
+
);
|
|
320
|
+
create index page_read_url on page_read(url, created_at);
|
|
321
|
+
`,
|
|
322
|
+
// v28: 截止与岗位身份 (ADR-041).
|
|
323
|
+
// - deadline_fact: what a source said about a deadline (for a post, a batch or a company) with its evidence;
|
|
324
|
+
// deadline_override: the person's own word (date / 招满即止 / 无固定截止 / 官方未写), which outranks every source.
|
|
325
|
+
// - position.source_key: the site's own id for a post. Posts were unique per (campaign, title), which merged three
|
|
326
|
+
// same-titled posts in three cities into one; identity is now (campaign, title, source_key). SQLite cannot drop a
|
|
327
|
+
// table constraint, so the table is rebuilt — with foreign keys off, as SQLite prescribes for this.
|
|
328
|
+
// - application.progress_url: where the applicant checks progress; reminder_sent: one row per reminder ever sent.
|
|
329
|
+
`/* foreign_keys:off */
|
|
330
|
+
create table position_new (
|
|
331
|
+
id text primary key, campaign_id text not null references campaign(id),
|
|
332
|
+
title text not null, jd_text text not null default '', cities text not null default '[]',
|
|
333
|
+
degree_req text not null default '', status text not null default 'open',
|
|
334
|
+
direction_tags text not null default '[]', last_seen_at text,
|
|
335
|
+
watch_priority integer, watch_note text not null default '',
|
|
336
|
+
created_at text not null, updated_at text not null,
|
|
337
|
+
major_req text not null default '', closes_at text, source_key text not null default ''
|
|
338
|
+
);
|
|
339
|
+
insert into position_new(id, campaign_id, title, jd_text, cities, degree_req, status, direction_tags, last_seen_at, watch_priority, watch_note, created_at, updated_at, major_req, closes_at)
|
|
340
|
+
select id, campaign_id, title, jd_text, cities, degree_req, status, direction_tags, last_seen_at, watch_priority, watch_note, created_at, updated_at, major_req, closes_at from position;
|
|
341
|
+
drop table position;
|
|
342
|
+
alter table position_new rename to position;
|
|
343
|
+
create unique index position_identity on position(campaign_id, title, source_key);
|
|
344
|
+
create table deadline_fact (
|
|
345
|
+
id text primary key, subject_type text not null, subject_id text not null, event text not null default 'application_close',
|
|
346
|
+
policy text not null, date_start text, date_end text, precision text not null default 'day', applies text not null default 'this_position',
|
|
347
|
+
grade text not null, source_kind text not null default '', url text not null default '', evidence text not null default '',
|
|
348
|
+
content_hash text not null, by text not null default '', created_at text not null, superseded_at text,
|
|
349
|
+
unique(subject_type, subject_id, content_hash)
|
|
350
|
+
);
|
|
351
|
+
create index deadline_fact_subject on deadline_fact(subject_type, subject_id);
|
|
352
|
+
create table deadline_override (
|
|
353
|
+
subject_type text not null, subject_id text not null, kind text not null, date text, note text not null default '',
|
|
354
|
+
by text not null default '', at text not null, primary key(subject_type, subject_id)
|
|
355
|
+
);
|
|
356
|
+
insert into deadline_override(subject_type, subject_id, kind, date, note, by, at)
|
|
357
|
+
select 'campaign', id, case when coalesce(closes_at_override, '') = '' then 'unknown' else 'date' end, nullif(closes_at_override, ''), override_note, '', overridden_at from campaign where overridden_at is not null;
|
|
358
|
+
alter table application add column progress_url text not null default '';
|
|
359
|
+
create table reminder_sent (key text primary key, sent_at text not null);
|
|
360
|
+
`,
|
|
361
|
+
// v29: deleting a campaign makes SQLite look for listings that still point at it (foreign key); without this index
|
|
362
|
+
// that was a scan of the whole listing table per campaign
|
|
363
|
+
`create index source_listing_campaign on source_listing(campaign_id);`,
|
|
364
|
+
// v30: the same for posts. v19 creates this index, but the line was added to v19 after the user's library had
|
|
365
|
+
// already run it, so that library never had it: there, deleting one post scanned all 12 万 listings (a tidy at start
|
|
366
|
+
// took five and a half minutes, 2026-10-13). A library made fresh already has the index.
|
|
367
|
+
`create index if not exists source_listing_position on source_listing(position_id);`,
|
|
368
|
+
// v31: the settings page asks for each subscription's last runs; with 1,500 subscriptions and no index on config_id
|
|
369
|
+
// that was 1,500 scans of the run history — 5 s for source.list on the user's library, the daemon answering nothing
|
|
370
|
+
// meanwhile, after every add / remove / update (2026-10-13; 0.02 s with the index)
|
|
371
|
+
`create index if not exists scan_run_config on scan_run(config_id, started_at);`,
|
|
372
|
+
// v32: ADR-045 — a write or a card made by a harness the daemon started for one chat turn carries that turn's id, so the
|
|
373
|
+
// drawer shows exactly this turn's doings (not whatever another caller did meanwhile)
|
|
374
|
+
`
|
|
375
|
+
alter table agent_action add column turn_id text;
|
|
376
|
+
alter table proposal add column turn_id text;
|
|
377
|
+
`,
|
|
378
|
+
// v33: ADR-046 — a chat turn answered by a harness's command line (Claude Code / Codex): JobOS's own record of what was
|
|
379
|
+
// asked and answered, by whom, what it cost; the harness keeps the full conversation under its session id
|
|
380
|
+
`
|
|
381
|
+
create table chat_turn (
|
|
382
|
+
id text primary key, backend text not null, session_id text not null default '', user_text text not null, answer text not null default '',
|
|
383
|
+
status text not null default 'running', cost_usd real, cost_kind text not null default 'unknown', error text not null default '',
|
|
384
|
+
started_at text not null, finished_at text
|
|
385
|
+
);
|
|
386
|
+
create index chat_turn_backend on chat_turn(backend, started_at);
|
|
387
|
+
`,
|
|
388
|
+
// v34: ADR-057 — model work goes back to the external agent whose call started it; which one is recorded
|
|
389
|
+
`
|
|
390
|
+
alter table model_job add column caller text not null default '';
|
|
391
|
+
`,
|
|
392
|
+
// v35: ADR-062 — a confirm card whose work answers later is marked while that work runs ('running'), so it never looks
|
|
393
|
+
// approved before the work is done; one left running by a stopped daemon is 'interrupted' at the next start and waits
|
|
394
|
+
// for the person. Null on every other card. (proposal is a small table: adding two columns touches no other table.)
|
|
395
|
+
`
|
|
396
|
+
alter table proposal add column execution text;
|
|
397
|
+
alter table proposal add column execution_started_at text;
|
|
398
|
+
`,
|
|
399
|
+
// v36: ADR-050 — every upgrade leaves a line here: when, from / to which schema, by which program, where the original went
|
|
400
|
+
`
|
|
401
|
+
create table upgrade_log (at text not null, from_v integer not null, to_v integer not null, app_version text not null, backup text not null default '');
|
|
402
|
+
`,
|
|
403
|
+
// v37: D-27 — BOSS 直聘 visits through the person's agent: one row per one-time permit (issued → used), and the stops
|
|
404
|
+
`
|
|
405
|
+
create table boss_visit (
|
|
406
|
+
id text primary key, session_id text not null, requested_at text not null, not_before text not null, expires_at text not null,
|
|
407
|
+
consumed_at text, target text not null, kind text not null, outcome text not null default 'issued', detail text not null default ''
|
|
408
|
+
);
|
|
409
|
+
create index boss_visit_at on boss_visit(requested_at);
|
|
410
|
+
create index boss_visit_session on boss_visit(session_id);
|
|
411
|
+
`,
|
|
412
|
+
// v38: D-28 — BOSS posts seen on a search page, kept as candidates until their job page is read (then position_id)
|
|
413
|
+
`
|
|
414
|
+
create table boss_candidate (
|
|
415
|
+
job_id text primary key, url text not null, title text not null, salary text not null default '', city text not null default '',
|
|
416
|
+
district text not null default '', experience text not null default '', degree text not null default '', days text not null default '',
|
|
417
|
+
months text not null default '', brand text not null default '', scale text not null default '', stage text not null default '',
|
|
418
|
+
industry text not null default '', skills text not null default '[]', labels text not null default '[]', query text not null default '',
|
|
419
|
+
city_code text not null default '', first_seen text not null, last_seen text not null, position_id text, detail_at text
|
|
420
|
+
);
|
|
421
|
+
create index boss_candidate_seen on boss_candidate(last_seen);
|
|
422
|
+
`,
|
|
423
|
+
// v39: D-29 — the A-share list and one industry's statements for a year, kept with the time they were fetched, so a
|
|
424
|
+
// comparison of employers can say which data it used and be run again on the same data
|
|
425
|
+
`
|
|
426
|
+
create table peer_snapshot (key text primary key, fetched_at text not null, data_json text not null);
|
|
427
|
+
`
|
|
428
|
+
];
|
|
429
|
+
var COUNTS_MAY_CHANGE = {};
|
|
430
|
+
var SCHEMA_VERSION = MIGRATIONS.length;
|
|
431
|
+
var SchemaMismatch = class extends Error {
|
|
432
|
+
constructor(code, have, message) {
|
|
433
|
+
super(message);
|
|
434
|
+
this.code = code;
|
|
435
|
+
this.have = have;
|
|
436
|
+
}
|
|
437
|
+
code;
|
|
438
|
+
have;
|
|
439
|
+
static {
|
|
440
|
+
__name(this, "SchemaMismatch");
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
function schemaOf(path) {
|
|
444
|
+
if (path === ":memory:" || !existsSync(path)) return 0;
|
|
445
|
+
const db = new DatabaseSync(path, { readOnly: true });
|
|
446
|
+
try {
|
|
447
|
+
const has = /* @__PURE__ */ __name((name) => !!db.prepare("select 1 from sqlite_master where type = 'table' and name = ?").get(name), "has");
|
|
448
|
+
if (!has("schema_version")) {
|
|
449
|
+
if (has("company")) throw new SchemaMismatch("NOT_OURS", 0, `这个数据库没有版本记录,不是 Zhiro / JobOS 建的:${path}`);
|
|
450
|
+
return 0;
|
|
451
|
+
}
|
|
452
|
+
const r = db.prepare("select count(*) as n, coalesce(max(v), 0) as v, coalesce(min(v), 1) as lo from schema_version").get();
|
|
453
|
+
if (r.n !== r.v || r.lo !== 1) throw new SchemaMismatch("GAPPED", r.v, `数据库的版本记录不连续(${r.n} 条,最高 v${r.v}),不打开:${path}`);
|
|
454
|
+
return r.v;
|
|
455
|
+
} finally {
|
|
456
|
+
db.close();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
__name(schemaOf, "schemaOf");
|
|
460
|
+
function openDb(path) {
|
|
461
|
+
const have = schemaOf(path);
|
|
462
|
+
if (have > SCHEMA_VERSION) throw new SchemaMismatch("TOO_NEW", have, `数据是更新的版本写的(数据库 v${have},这个程序只认到 v${SCHEMA_VERSION})。请装新版本,或用 \`zhiro vault rollback\` 回到升级前`);
|
|
463
|
+
if (have > 0 && have < SCHEMA_VERSION) throw new SchemaMismatch("NEEDS_UPGRADE", have, `数据库是 v${have},需要先升级到 v${SCHEMA_VERSION}:启动一次 Zhiro,或运行 \`zhiro vault upgrade\``);
|
|
464
|
+
if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true });
|
|
465
|
+
const db = new DatabaseSync(path);
|
|
466
|
+
db.exec("pragma journal_mode = wal; pragma foreign_keys = on;");
|
|
467
|
+
if (have === 0) migrate(db);
|
|
468
|
+
return db;
|
|
469
|
+
}
|
|
470
|
+
__name(openDb, "openDb");
|
|
471
|
+
function migrate(db, migrations = MIGRATIONS) {
|
|
472
|
+
db.exec("create table if not exists schema_version (v integer primary key)");
|
|
473
|
+
const row = db.prepare("select max(v) as v from schema_version").get();
|
|
474
|
+
const current = row.v ?? 0;
|
|
475
|
+
for (let i = current; i < migrations.length; i++) {
|
|
476
|
+
const rebuild = migrations[i].includes("/* foreign_keys:off */");
|
|
477
|
+
if (rebuild) db.exec("pragma foreign_keys = off");
|
|
478
|
+
db.exec("begin");
|
|
479
|
+
try {
|
|
480
|
+
db.exec(migrations[i]);
|
|
481
|
+
db.prepare("insert into schema_version(v) values (?)").run(i + 1);
|
|
482
|
+
db.exec("commit");
|
|
483
|
+
} catch (e) {
|
|
484
|
+
db.exec("rollback");
|
|
485
|
+
throw e;
|
|
486
|
+
}
|
|
487
|
+
if (rebuild) {
|
|
488
|
+
db.exec("pragma foreign_keys = on");
|
|
489
|
+
const broken = db.prepare("pragma foreign_key_check").all();
|
|
490
|
+
if (broken.length) throw new Error(`migration v${i + 1} left ${broken.length} dangling references`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
__name(migrate, "migrate");
|
|
495
|
+
function now() {
|
|
496
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
497
|
+
}
|
|
498
|
+
__name(now, "now");
|
|
499
|
+
var depth = /* @__PURE__ */ new WeakMap();
|
|
500
|
+
function tx(db, fn) {
|
|
501
|
+
const d = depth.get(db) ?? 0;
|
|
502
|
+
depth.set(db, d + 1);
|
|
503
|
+
const sp = `sp${d}`;
|
|
504
|
+
db.exec(d === 0 ? "begin immediate" : `savepoint ${sp}`);
|
|
505
|
+
try {
|
|
506
|
+
const r = fn();
|
|
507
|
+
db.exec(d === 0 ? "commit" : `release ${sp}`);
|
|
508
|
+
return r;
|
|
509
|
+
} catch (e) {
|
|
510
|
+
db.exec(d === 0 ? "rollback" : `rollback to ${sp}; release ${sp}`);
|
|
511
|
+
throw e;
|
|
512
|
+
} finally {
|
|
513
|
+
depth.set(db, d);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
__name(tx, "tx");
|
|
517
|
+
var json = {
|
|
518
|
+
parse(s, fallback) {
|
|
519
|
+
if (!s) return fallback;
|
|
520
|
+
try {
|
|
521
|
+
return JSON.parse(s);
|
|
522
|
+
} catch {
|
|
523
|
+
return fallback;
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
str(v) {
|
|
527
|
+
return JSON.stringify(v ?? null);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
export {
|
|
532
|
+
MIGRATIONS,
|
|
533
|
+
COUNTS_MAY_CHANGE,
|
|
534
|
+
SCHEMA_VERSION,
|
|
535
|
+
SchemaMismatch,
|
|
536
|
+
schemaOf,
|
|
537
|
+
openDb,
|
|
538
|
+
migrate,
|
|
539
|
+
now,
|
|
540
|
+
tx,
|
|
541
|
+
json
|
|
542
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name,
|
|
3
|
+
dataDir,
|
|
4
|
+
ensureEntry
|
|
5
|
+
} from "./chunk-2SSYR7VW.js";
|
|
6
|
+
|
|
7
|
+
// apps/daemon/src/config.ts
|
|
8
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, writeSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { randomBytes } from "node:crypto";
|
|
11
|
+
import YAML from "yaml";
|
|
12
|
+
var DEFAULTS = {
|
|
13
|
+
vault: "",
|
|
14
|
+
http: { host: "127.0.0.1", port: 8787 },
|
|
15
|
+
llm: { provider: "openai-codex", model: "gpt-6-astra" },
|
|
16
|
+
feishu: {},
|
|
17
|
+
reminders: { daysBefore: [7, 3, 1] },
|
|
18
|
+
agent: { mode: "default", maxWritesPerTurn: 10, harnessConfirm: "harness", backend: "builtin" },
|
|
19
|
+
sources: []
|
|
20
|
+
};
|
|
21
|
+
function defaultVaultDir() {
|
|
22
|
+
return dataDir().dir;
|
|
23
|
+
}
|
|
24
|
+
__name(defaultVaultDir, "defaultVaultDir");
|
|
25
|
+
function loadConfig(vault = defaultVaultDir()) {
|
|
26
|
+
ensureEntry(vault);
|
|
27
|
+
for (const d of ["config", "captures", "snapshots", "submissions", "backups", "runs", "profile", "resume/templates", "resume/variants", "research"]) {
|
|
28
|
+
mkdirSync(join(vault, d), { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
const file = configFile(vault);
|
|
31
|
+
let user = {};
|
|
32
|
+
if (existsSync(file)) user = YAML.parse(readFileSync(file, "utf8")) ?? {};
|
|
33
|
+
else replaceConfig(vault, { ...DEFAULTS, vault: void 0 });
|
|
34
|
+
return {
|
|
35
|
+
...DEFAULTS,
|
|
36
|
+
...user,
|
|
37
|
+
vault,
|
|
38
|
+
// ZHIRO_PORT: another port for this run (8787 taken, a second instance for a test) without touching the settings
|
|
39
|
+
http: { ...DEFAULTS.http, ...user.http ?? {}, ...process.env.ZHIRO_PORT ? { port: Number(process.env.ZHIRO_PORT) } : {} },
|
|
40
|
+
llm: { ...DEFAULTS.llm, ...user.llm ?? {} },
|
|
41
|
+
feishu: { ...user.feishu ?? {} },
|
|
42
|
+
reminders: { ...DEFAULTS.reminders, ...user.reminders ?? {} },
|
|
43
|
+
agent: { ...DEFAULTS.agent, ...user.agent ?? {} },
|
|
44
|
+
sources: Array.isArray(user.sources) ? user.sources : []
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
__name(loadConfig, "loadConfig");
|
|
48
|
+
var configFile = /* @__PURE__ */ __name((vault) => join(vault, "config", "jobos.yaml"), "configFile");
|
|
49
|
+
function writeWhole(vault, doc) {
|
|
50
|
+
const file = configFile(vault);
|
|
51
|
+
const tmp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
52
|
+
try {
|
|
53
|
+
const fd = openSync(tmp, "wx");
|
|
54
|
+
try {
|
|
55
|
+
writeSync(fd, YAML.stringify(doc), null, "utf8");
|
|
56
|
+
fsyncSync(fd);
|
|
57
|
+
} finally {
|
|
58
|
+
closeSync(fd);
|
|
59
|
+
}
|
|
60
|
+
renameSync(tmp, file);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
rmSync(tmp, { force: true });
|
|
63
|
+
throw e;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
__name(writeWhole, "writeWhole");
|
|
67
|
+
function updateConfig(vault, change) {
|
|
68
|
+
const file = configFile(vault);
|
|
69
|
+
let doc = {};
|
|
70
|
+
try {
|
|
71
|
+
doc = YAML.parse(readFileSync(file, "utf8")) ?? {};
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (e?.code !== "ENOENT") throw e;
|
|
74
|
+
}
|
|
75
|
+
change(doc);
|
|
76
|
+
writeWhole(vault, doc);
|
|
77
|
+
}
|
|
78
|
+
__name(updateConfig, "updateConfig");
|
|
79
|
+
function replaceConfig(vault, doc) {
|
|
80
|
+
writeWhole(vault, doc);
|
|
81
|
+
}
|
|
82
|
+
__name(replaceConfig, "replaceConfig");
|
|
83
|
+
function clearConfigTemps(vault) {
|
|
84
|
+
const dir = join(vault, "config");
|
|
85
|
+
let names = [];
|
|
86
|
+
try {
|
|
87
|
+
names = readdirSync(dir);
|
|
88
|
+
} catch {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
for (const f of names) if (/^jobos\.yaml\.(\d+\.[0-9a-f]+\.)?tmp$/.test(f)) rmSync(join(dir, f), { force: true });
|
|
92
|
+
}
|
|
93
|
+
__name(clearConfigTemps, "clearConfigTemps");
|
|
94
|
+
function dbPath(vault) {
|
|
95
|
+
return join(vault, "jobos.db");
|
|
96
|
+
}
|
|
97
|
+
__name(dbPath, "dbPath");
|
|
98
|
+
|
|
99
|
+
export {
|
|
100
|
+
defaultVaultDir,
|
|
101
|
+
loadConfig,
|
|
102
|
+
configFile,
|
|
103
|
+
updateConfig,
|
|
104
|
+
replaceConfig,
|
|
105
|
+
clearConfigTemps,
|
|
106
|
+
dbPath
|
|
107
|
+
};
|