@chance722/dsh-inbox 0.2.4 → 0.2.6
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/README.md +11 -2
- package/README.zh.md +11 -2
- package/cordis.patch.yml +21 -0
- package/lib/client.js +9 -33
- package/lib/index.js +129 -36
- package/lib/types/client/i18n.d.ts +0 -11
- package/lib/types/client/messages.d.ts +2 -14
- package/lib/types/host/link-title.d.ts +25 -1
- package/lib/types/host/remote/merge.d.ts +9 -0
- package/lib/types/shared/panel-wire.d.ts +2 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -19,9 +19,9 @@ It works for you alone: everything lands on your machine, and the model only see
|
|
|
19
19
|
| Feature | What it does |
|
|
20
20
|
|---|---|
|
|
21
21
|
| **Two ways in** | `/inbox <text or link>` in the composer (attach images to carry them along), or paste / drop / pick a file in the panel |
|
|
22
|
-
| **Automatic classification** | Links are filed by platform and media type (
|
|
22
|
+
| **Automatic classification** | Links are filed by platform and media type (Bilibili/YouTube videos, WeChat/掘金/Zhihu articles — 60-odd sites), text by credential shape, images get a 疑似证件 tag when they have card proportions; what the rules cannot decide goes to the model, with a daily cap |
|
|
23
23
|
| **Panel** | Filter by watch-later / category / tag, search across title, text, link and note, switch between two list densities; the detail pane edits name, category, note and tags, flags watch-later, deletes and restores; it follows dsh's dark and light themes **and its language** (switch dsh to English under 设置 → 常规 and the whole panel, dock, cards and manual come with you) |
|
|
24
|
-
| **Everything has a name** | A link is named by the headline of the page it points at (no model tokens spent), a photo or file by the name it arrived with, and the name you type always wins. The row shows the name; the glyph on the left says what kind of thing it is (a key for a credential) |
|
|
24
|
+
| **Everything has a name** | A link is named by the headline of the page it points at (no model tokens spent), a photo or file by the name it arrived with, and the name you type always wins. A page that will not be read — an anti-bot page, a dead link — leaves the address itself as the name rather than a guess. The row shows the name; the glyph on the left says what kind of thing it is (a key for a credential) |
|
|
25
25
|
| **Who judged the category** | A coloured badge next to the category: 规则判定 (grey, the local rules) / 模型判定 (violet, the capped model pass) / 手动判定 (blue, your own choice — nothing overwrites it later) |
|
|
26
26
|
| **Ask in conversation** | Ask for 收件箱 / 仓库 / inbox and the assistant searches by words, category, tag, watch-later flag or kind (ten at a time plus a count of the rest, with thumbnails right in the results), then opens one by id (text up to 1000 characters, link, note, tags, attachment facts). To look at one yourself: expand 「N 次工具调用」 above the answer and press 打开 ↗ — the 仓库 tab opens on that record |
|
|
27
27
|
| **Looking at a picture** | Image bytes stay out of the conversation by default; when you ask "look at this picture and tell me what it is", the assistant sends that one image to itself — explicitly, per call |
|
|
@@ -161,6 +161,7 @@ With a remote configured and `/inbox` as the directory:
|
|
|
161
161
|
- **Pictures**: classification does send an image to the model (a phone photo of an ID card has the same proportions as any other photo); the conversation gets an `[attachment:id]` marker by default. **The one exception**: when you explicitly ask the assistant to look at a picture, that single image is sent to it — per call, never by default.
|
|
162
162
|
- **The model cannot see your vault** unless you ask it to look, and classification requests are redacted first.
|
|
163
163
|
- **Your remote needs access control**: only credential bodies are ciphertext up there — text, links, notes and attachment bytes are in the clear, and the master password and key never sync.
|
|
164
|
+
- **What the install changes outside the panel**: reading a pasted link's headline is this plugin's only outbound request — one GET, only for links captured on this machine, never for links that arrived through sync. The profile makes it with a browser-shaped identity (`… AppleWebKit/537.36 (KHTML, like Gecko) dsh-inbox Safari/537.36`), shipped in this package's `cordis.patch.yml` as an override of `web-fetch-http.userAgent`, because sites gate on the shape of that string. That identity is the whole profile's, the model's own web tools included; **your own `cordis.patch.yml` overrides it**. Measurements and reasoning: [docs/help/link-title-fetch.md](docs/help/link-title-fetch.md).
|
|
164
165
|
|
|
165
166
|
## Development
|
|
166
167
|
|
|
@@ -173,6 +174,14 @@ pnpm test # vitest
|
|
|
173
174
|
|
|
174
175
|
Client-side changes need `pnpm build` and a page reload (dsh's client-hmr reloads it for you); host-side changes need a restart. Details in [docs/help/dev-setup.md](docs/help/dev-setup.md); milestones and acceptance records in the [development bus](docs/feature/dev-bus.md).
|
|
175
176
|
|
|
177
|
+
To switch the profile you actually run between the **published package** and **this checkout** (default profile `web`; set `DSH_PROFILE` for another):
|
|
178
|
+
|
|
179
|
+
```powershell
|
|
180
|
+
pnpm dev:status # which one is in use right now
|
|
181
|
+
pnpm dev:npm # point it at the published version
|
|
182
|
+
pnpm dev:local # point it back here (builds first, then links)
|
|
183
|
+
```
|
|
184
|
+
|
|
176
185
|
## License
|
|
177
186
|
|
|
178
187
|
MIT
|
package/README.zh.md
CHANGED
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
| 功能 | 说明 |
|
|
20
20
|
|---|---|
|
|
21
21
|
| **两个入口** | 输入框里 `/inbox <文字或链接>`(图片附在输入框上一起收),或面板里粘贴 / 拖拽 / 选择文件 |
|
|
22
|
-
| **自动分类** | 链接按平台与类型分(B
|
|
22
|
+
| **自动分类** | 链接按平台与类型分(B 站/YouTube 视频,公众号/掘金/知乎文章…共 60 多个站点)、文本按密钥形状分、图片按比例加「疑似证件」标签;规则判不出的交给模型兜底,有每日上限 |
|
|
23
23
|
| **面板** | 待看 / 类目 / 标签筛选 + 跨标题正文链接备注的搜索;两种列表密度;详情里能改名称、类目、备注、标签,能标待看,能删除与恢复;**跟着 dsh 的深色/浅色主题走,也跟着它的语言走**(在「设置 → 常规」里把 dsh 切成 English,面板、右侧 dock、对话卡片与使用手册一起变) |
|
|
24
|
-
| **每条都有名字** | 链接自动抓页面标题(不花模型 token
|
|
24
|
+
| **每条都有名字** | 链接自动抓页面标题(不花模型 token)、图片文件用原文件名;你自己起的名字永远优先。读不到的页面——对方是反爬页、或者链接已经死了——名字位就留链接本身,不猜一个。列表里显示名字本身,类型交给左侧图标(密钥就是一把钥匙) |
|
|
25
25
|
| **类目是谁判的** | 类目旁带颜色标签:规则判定(灰)/ 模型判定(紫)/ 手动判定(蓝)。你改过的类目,后续任何流程都不会覆盖 |
|
|
26
26
|
| **对话里取回** | 直接问「我的收件箱 / 仓库 / inbox 里有哪些还没看的链接」就行,助手按关键词/类目/标签/待看/类型查(最多 10 条 + 还剩几条,带图的结果直接显示缩略图),按 id 打开一条(正文最多 1000 字、链接、备注、标签、附件信息)。想直接看某一条:展开回答上方的「N 次工具调用」,记录后面的「打开 ↗」会在右侧「仓库」里停到那一条 |
|
|
27
27
|
| **看图** | 默认图片字节不进对话;你说「帮我看这张图是什么」时,助手才会把那张图发给自己看(一次性、显式要) |
|
|
@@ -160,6 +160,7 @@ rm -r ~/.dsh/profiles/<你装的 profile>
|
|
|
160
160
|
- **图片**:分类时会把图片发给模型判断(手机拍的证件照比例与普通照片无异);对话里默认只回 `[attachment:id]` 标记,字节不进对话。**唯一的例外**:你明确让助手「看这张图」时,它会把那一张发给自己看——每次都要显式要求,默认永不发。
|
|
161
161
|
- **模型看不到你的仓库**,除非你让它查(它调工具时才读得到),且分类请求先过脱敏。
|
|
162
162
|
- **云端要有访问控制**:同步上去的内容里只有账密正文是密文,其余(文本、链接、备注、附件字节)是明文;主密码与密钥从不同步。
|
|
163
|
+
- **装它会改到面板之外的一处**:抓链接标题是本插件唯一的对外请求——一次 GET,只在本机捕获的链接上发,同步拉进来的链接不抓。抓取身份是**浏览器形状**(`… AppleWebKit/537.36 (KHTML, like Gecko) dsh-inbox Safari/537.36`),由本包的 `cordis.patch.yml` 覆盖 `web-fetch-http.userAgent` 写成,因为站点按这个字符串的**形状**认客户端。这层身份是整个 profile 的,模型的 web 工具也一起用;**你自己的 `cordis.patch.yml` 覆盖得掉**。实测与理由见 [docs/help/link-title-fetch.md](docs/help/link-title-fetch.md)。
|
|
163
164
|
|
|
164
165
|
## 开发
|
|
165
166
|
|
|
@@ -172,6 +173,14 @@ pnpm test # vitest
|
|
|
172
173
|
|
|
173
174
|
改客户端代码:`pnpm build` 后刷新页面即可(dsh 的 client-hmr 会自己重载);改宿主代码要重启服务。细节见 [docs/help/dev-setup.md](docs/help/dev-setup.md),阶段与验收记录在[开发总线](docs/feature/dev-bus.md)。
|
|
174
175
|
|
|
176
|
+
日常想在**线上发布版**和**本地改动**之间切换(默认 profile 是 `web`,换 profile 加 `DSH_PROFILE`):
|
|
177
|
+
|
|
178
|
+
```powershell
|
|
179
|
+
pnpm dev:status # 现在用的是哪一个:本仓库 / 线上包
|
|
180
|
+
pnpm dev:npm # 切到 npm 上发布的版本(体验发布版)
|
|
181
|
+
pnpm dev:local # 切回当前仓库(会先 pnpm build 再链接)
|
|
182
|
+
```
|
|
183
|
+
|
|
175
184
|
## 许可
|
|
176
185
|
|
|
177
186
|
MIT
|
package/cordis.patch.yml
CHANGED
|
@@ -9,3 +9,24 @@
|
|
|
9
9
|
- insert:
|
|
10
10
|
- id: dsh-inbox
|
|
11
11
|
name: '@chance722/dsh-inbox'
|
|
12
|
+
|
|
13
|
+
# The identity this profile fetches pages with, restated here because our own
|
|
14
|
+
# link-title fetch needs a browser-shaped one: sites gate on the *shape* of this
|
|
15
|
+
# string, and bilibili answers the `Mozilla/5.0 (compatible; ...)` form with an
|
|
16
|
+
# anti-bot page that carries a real-looking `<title>` (measured 2026-09-21:
|
|
17
|
+
# 20/29 refused for the compatible form, 0/15 for the one below). Details,
|
|
18
|
+
# measurements and the refusal heuristics live in docs/help/link-title-fetch.md.
|
|
19
|
+
#
|
|
20
|
+
# It still says who is asking — `dsh-inbox` sits where a browser puts
|
|
21
|
+
# `Chrome/140.0.0.0` — with no version token, because one that goes stale on every
|
|
22
|
+
# release is worse than none. A parenthetical `(+url)` or a suffix after a browser
|
|
23
|
+
# token lands in the refused bucket exactly like `(compatible; ...)` does, so
|
|
24
|
+
# there is none of either.
|
|
25
|
+
#
|
|
26
|
+
# Scope: this is the profile's fetch identity, so the model's own web tools use it
|
|
27
|
+
# too. The user's own cordis.patch.yml is applied after this layer and wins,
|
|
28
|
+
# which is how anyone picks a different identity without touching the package.
|
|
29
|
+
- id: web-fetch-http
|
|
30
|
+
name: '@deepseek-ai/dsh-web-fetch-http'
|
|
31
|
+
config:
|
|
32
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) dsh-inbox Safari/537.36'
|
package/lib/client.js
CHANGED
|
@@ -715,12 +715,6 @@ window.__ModuleLoader__.load({
|
|
|
715
715
|
"source.rule.hint": "\u672C\u5730\u89C4\u5219\u6309\u94FE\u63A5\u3001\u6587\u672C\u3001\u56FE\u7247\u7684\u5F62\u72B6\u5224\u7684\uFF0C\u6CA1\u6709\u8054\u7F51",
|
|
716
716
|
"source.model.hint": "\u89C4\u5219\u5224\u4E0D\u51FA\u6765\u624D\u4EA4\u7ED9\u6A21\u578B\u5224\u7684\uFF1B\u4F60\u8BF4\u7684\u8BDD\u6C38\u8FDC\u4F18\u5148\uFF0C\u968F\u65F6\u53EF\u4EE5\u6539",
|
|
717
717
|
"source.user.hint": "\u4F60\u81EA\u5DF1\u9009\u7684\u7C7B\u76EE\uFF0C\u89C4\u5219\u548C\u6A21\u578B\u90FD\u4E0D\u4F1A\u8986\u76D6\u5B83",
|
|
718
|
-
// Why a link has no fetched title (these reach the panel as `linkTitleError`).
|
|
719
|
-
"link.noTitle": "\u90A3\u4E2A\u9875\u9762\u91CC\u6CA1\u6709\u6807\u9898\uFF08\u6709\u4E9B\u7AD9\u70B9\u5BF9\u975E\u6D4F\u89C8\u5668\u7684\u8BF7\u6C42\u53EA\u56DE\u7A7A\u58F3\u9875\uFF0C\u6BD4\u5982\u5FAE\u4FE1\uFF09",
|
|
720
|
-
"link.http": "\u5BF9\u65B9\u8FD4\u56DE HTTP {status}",
|
|
721
|
-
"link.notHtml": "\u90A3\u4E2A\u5730\u5740\u4E0D\u662F\u7F51\u9875",
|
|
722
|
-
"link.unreachable": "\u8BF7\u6C42\u6CA1\u6210\u529F\uFF08\u7F51\u7EDC\u4E0D\u901A\u6216\u5BF9\u65B9\u62D2\u7EDD\uFF09",
|
|
723
|
-
"link.unknown": "\u539F\u56E0\u4E0D\u660E",
|
|
724
718
|
// Chrome above the list.
|
|
725
719
|
"app.settings": "\u8BBE\u7F6E",
|
|
726
720
|
"app.manual": "\u4F7F\u7528\u624B\u518C",
|
|
@@ -777,7 +771,6 @@ window.__ModuleLoader__.load({
|
|
|
777
771
|
"detail.removeTag": "\u4ECE\u8FD9\u6761\u8BB0\u5F55\u4E0A\u79FB\u9664\u300C{tag}\u300D",
|
|
778
772
|
"detail.updated": " \xB7 \u66F4\u65B0 {when}",
|
|
779
773
|
"detail.sealedNote": "\u8FD9\u6761\u8D26\u5BC6\u7684\u6B63\u6587\u662F\u5BC6\u6587\uFF0C\u73B0\u5728\u89E3\u4E0D\u5F00\u3002\u5230\u300C\u8BBE\u7F6E \u2192 \u8D26\u5BC6\u52A0\u5BC6\u300D\u89E3\u9501\uFF08\u6216\u5148\u8BBE\u4E00\u4E2A\u4E3B\u5BC6\u7801\uFF09\u5C31\u80FD\u770B\u5230\u3002",
|
|
780
|
-
"detail.titleMissed": "\u6CA1\u6293\u5230\u9875\u9762\u6807\u9898\uFF1A{reason}\u3002\u53EF\u4EE5\u81EA\u5DF1\u8D77\u4E2A\u540D\u5B57\u3002",
|
|
781
774
|
"detail.play": "\u64AD\u653E",
|
|
782
775
|
"detail.zoom": "\u653E\u5927\u67E5\u770B",
|
|
783
776
|
"detail.playInBrowser": "\u5728\u6D4F\u89C8\u5668\u91CC\u64AD\u653E",
|
|
@@ -868,11 +861,14 @@ window.__ModuleLoader__.load({
|
|
|
868
861
|
"sync.pullFailed": "\u62C9\u53D6\u5931\u8D25\uFF1A{reason}",
|
|
869
862
|
// The refresh button's tooltip: counts first, reasons only when something was
|
|
870
863
|
// skipped. The toast carries the two numbers alone.
|
|
871
|
-
|
|
864
|
+
// No number here on purpose: the push considers every row it has, tombstones
|
|
865
|
+
// included, so "26 条已是最新" next to a header saying "共 13 条" is a
|
|
866
|
+
// mismatch the reader has to resolve for themselves (2026-09-21).
|
|
867
|
+
"sync.detailPushedIdle": "\u63A8\u9001 0 \u6761\uFF08\u6CA1\u6709\u65B0\u6539\u52A8\uFF09",
|
|
872
868
|
"sync.detailPushed": "\u63A8\u9001 {records} \u6761 / {attachments} \u4E2A\u9644\u4EF6",
|
|
873
869
|
"sync.detailPulled": "\u62C9\u53D6 {count} \u6761 \xB7 \u4E91\u7AEF {records} \u6761\u8BB0\u5F55 / {files} \u4E2A\u9644\u4EF6",
|
|
874
870
|
"sync.detailAdded": "\uFF08\u5176\u4E2D {count} \u6761\u662F\u65B0\u8BB0\u5F55\uFF09",
|
|
875
|
-
"sync.
|
|
871
|
+
"sync.detailDeleted": "\uFF08\u5176\u4E2D {count} \u6761\u662F\u5220\u9664\uFF0C\u4F1A\u8FDB\u56DE\u6536\u7AD9\uFF09",
|
|
876
872
|
"sync.pullForeignSync": " \xB7 \u26A0\uFE0F \u53E6\u6709 {records} \u6761\u8BB0\u5F55\u5728\u522B\u7684\u540C\u6B65\u76EE\u5F55\uFF1A{roots}",
|
|
877
873
|
"sync.detailForeign": " \xB7 \u26A0\uFE0F \u522B\u7684\u540C\u6B65\u76EE\u5F55\uFF1A{roots}\uFF08{records} \u6761\u8BB0\u5F55\uFF1B\u672C\u673A {ours}\uFF09",
|
|
878
874
|
// The toast: two numbers, nothing else. Everything else about a sync is
|
|
@@ -996,11 +992,6 @@ window.__ModuleLoader__.load({
|
|
|
996
992
|
"source.rule.hint": "the local rules judged this from the shape of the link, text or image \u2014 no network was used",
|
|
997
993
|
"source.model.hint": "the rules could not decide, so the model did; what you say always wins and can be changed any time",
|
|
998
994
|
"source.user.hint": "the category you chose yourself \u2014 neither the rules nor the model will overwrite it",
|
|
999
|
-
"link.noTitle": "that page has no title (some sites answer non-browser requests with an empty shell \u2014 WeChat does)",
|
|
1000
|
-
"link.http": "the other side answered HTTP {status}",
|
|
1001
|
-
"link.notHtml": "that address is not a web page",
|
|
1002
|
-
"link.unreachable": "the request did not go through (no network, or refused)",
|
|
1003
|
-
"link.unknown": "no clear reason",
|
|
1004
995
|
"app.settings": "Settings",
|
|
1005
996
|
"app.manual": "Manual",
|
|
1006
997
|
"app.counts": " \xB7 {total} total \xB7 {watch} to read \xB7 {deleted} in the bin",
|
|
@@ -1053,7 +1044,6 @@ window.__ModuleLoader__.load({
|
|
|
1053
1044
|
"detail.removeTag": "Remove \u201C{tag}\u201D from this record",
|
|
1054
1045
|
"detail.updated": " \xB7 updated {when}",
|
|
1055
1046
|
"detail.sealedNote": "This credential\u2019s body is ciphertext and cannot be read right now. Unlock it under Settings \u2192 Credential encryption (or set a master password first).",
|
|
1056
|
-
"detail.titleMissed": "No page title was fetched: {reason}. You can give it a name yourself.",
|
|
1057
1047
|
"detail.play": "Play",
|
|
1058
1048
|
"detail.zoom": "Zoom in",
|
|
1059
1049
|
"detail.playInBrowser": "Play in the browser",
|
|
@@ -1137,11 +1127,11 @@ window.__ModuleLoader__.load({
|
|
|
1137
1127
|
"sync.pushPartial": "{head} \xB7 partly failed: {reason}",
|
|
1138
1128
|
"sync.pullNoAddress": "No address configured yet",
|
|
1139
1129
|
"sync.pullFailed": "Pull failed: {reason}",
|
|
1140
|
-
"sync.detailPushedIdle": "pushed 0 (
|
|
1130
|
+
"sync.detailPushedIdle": "pushed 0 (nothing new)",
|
|
1141
1131
|
"sync.detailPushed": "pushed {records} / {attachments} attachments",
|
|
1142
1132
|
"sync.detailPulled": "pulled {count} \xB7 the cloud holds {records} records / {files} attachments",
|
|
1143
1133
|
"sync.detailAdded": " ({count} of them new)",
|
|
1144
|
-
"sync.
|
|
1134
|
+
"sync.detailDeleted": " ({count} of them deletions \u2014 they land in the recycle bin)",
|
|
1145
1135
|
"sync.pullForeignSync": " \xB7 \u26A0\uFE0F {records} records live in another sync directory: {roots}",
|
|
1146
1136
|
"sync.detailForeign": " \xB7 \u26A0\uFE0F another sync directory: {roots} ({records} records; ours is {ours})",
|
|
1147
1137
|
"sync.shortPushed": "pushed {count}",
|
|
@@ -2022,15 +2012,6 @@ window.__ModuleLoader__.load({
|
|
|
2022
2012
|
var ACCENT_COLOR = "#6e9ef7";
|
|
2023
2013
|
var MODEL_COLOR = "#a78bfa";
|
|
2024
2014
|
var CONTROL_HEIGHT = "calc(1.6em + 12px)";
|
|
2025
|
-
function titleFailureText(code) {
|
|
2026
|
-
if (code === "no-title") {
|
|
2027
|
-
return t("link.noTitle");
|
|
2028
|
-
}
|
|
2029
|
-
if (code.startsWith("http:")) return t("link.http", { status: code.slice("http:".length) });
|
|
2030
|
-
if (code.startsWith("not-html:")) return t("link.notHtml");
|
|
2031
|
-
if (code.startsWith("network:")) return t("link.unreachable");
|
|
2032
|
-
return t("link.unknown");
|
|
2033
|
-
}
|
|
2034
2015
|
var paneRowStyle = { flex: "none" };
|
|
2035
2016
|
var selectStyle = {
|
|
2036
2017
|
...actionStyle,
|
|
@@ -3316,12 +3297,8 @@ window.__ModuleLoader__.load({
|
|
|
3316
3297
|
files: result.remoteAttachments ?? 0
|
|
3317
3298
|
});
|
|
3318
3299
|
const gained = (result.added ?? 0) === 0 ? "" : t("sync.detailAdded", { count: result.added ?? 0 });
|
|
3319
|
-
const
|
|
3320
|
-
|
|
3321
|
-
sync: result.skippedSync ?? 0,
|
|
3322
|
-
older: result.skippedOlder ?? 0
|
|
3323
|
-
});
|
|
3324
|
-
return `${head}${gained}${skipped}${warningsOf(result, true)}`;
|
|
3300
|
+
const deleted = (result.deletions ?? 0) === 0 ? "" : t("sync.detailDeleted", { count: result.deletions ?? 0 });
|
|
3301
|
+
return `${head}${gained}${deleted}${warningsOf(result, true)}`;
|
|
3325
3302
|
}
|
|
3326
3303
|
function EncryptionSettings({ call }) {
|
|
3327
3304
|
const [status, setStatus] = import_react8.default.useState();
|
|
@@ -4324,7 +4301,6 @@ window.__ModuleLoader__.load({
|
|
|
4324
4301
|
style: { ...paneRowStyle, ...inputStyle, width: "100%", boxSizing: "border-box" }
|
|
4325
4302
|
}
|
|
4326
4303
|
),
|
|
4327
|
-
detail.title === void 0 && detail.linkTitle === void 0 && detail.linkTitleError !== void 0 && detail.kind === "link" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: { ...paneRowStyle, margin: 0, fontSize: 12, opacity: 0.6 }, children: t("detail.titleMissed", { reason: titleFailureText(detail.linkTitleError) }) }),
|
|
4328
4304
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
4329
4305
|
SelectBox,
|
|
4330
4306
|
{
|
package/lib/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { defineTool as defineTool2 } from "@deepseek-ai/dsh-tools";
|
|
|
3
3
|
|
|
4
4
|
// src/shared/constants.ts
|
|
5
5
|
var PACKAGE_NAME = "@chance722/dsh-inbox";
|
|
6
|
-
var VERSION = true ? "0.2.
|
|
6
|
+
var VERSION = true ? "0.2.6" : "0.0.0-dev";
|
|
7
7
|
var DEFAULT_USER_AGENT = "dsh-inbox";
|
|
8
8
|
|
|
9
9
|
// src/shared/vocabulary.ts
|
|
@@ -208,6 +208,16 @@ var ENTITIES = {
|
|
|
208
208
|
apos: "'",
|
|
209
209
|
nbsp: " "
|
|
210
210
|
};
|
|
211
|
+
var REFUSAL_MARKER = /(risk-captcha|_BiliGreyResult|cf-chl|challenge-platform|geetest)/i;
|
|
212
|
+
var REFUSAL_TITLE = /(验证码|人机验证|安全验证|环境异常|访问异常|请完成验证|正在验证|captcha|just a moment|attention required|access denied|forbidden)/i;
|
|
213
|
+
var REFUSAL_TEXT_FLOOR = 200;
|
|
214
|
+
function visibleTextLength(html) {
|
|
215
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<!--[\s\S]*?-->/g, "").replace(/<[^>]+>/g, "").replace(/\s+/g, "").length;
|
|
216
|
+
}
|
|
217
|
+
function looksLikeRefusal(html, title) {
|
|
218
|
+
if (visibleTextLength(html) >= REFUSAL_TEXT_FLOOR) return false;
|
|
219
|
+
return REFUSAL_MARKER.test(html) || REFUSAL_TITLE.test(title);
|
|
220
|
+
}
|
|
211
221
|
function decodeEntities(text) {
|
|
212
222
|
return text.replace(/&(#[xX]?[0-9a-fA-F]+|[a-zA-Z]+);/g, (whole, body) => {
|
|
213
223
|
if (body.startsWith("#")) {
|
|
@@ -277,6 +287,11 @@ async function fetchLinkTitle(vault, id, web, log = () => {
|
|
|
277
287
|
log(`${host}\uFF1A\u9875\u9762\u91CC\u6CA1\u6709 <title>\uFF08${String(result.body.content.length)} \u5B57\u7B26\uFF09`);
|
|
278
288
|
return void 0;
|
|
279
289
|
}
|
|
290
|
+
if (looksLikeRefusal(result.body.content, title)) {
|
|
291
|
+
await miss(vault, id, "refused-page");
|
|
292
|
+
log(`${host}\uFF1A\u6293\u5230\u7684\u662F\u62D2\u7EDD\u9875\uFF0C\u4E0D\u5F53\u540D\u5B57`);
|
|
293
|
+
return void 0;
|
|
294
|
+
}
|
|
280
295
|
const current = vault.get(id);
|
|
281
296
|
if (current === void 0 || current.title !== void 0 || current.linkTitle !== void 0) {
|
|
282
297
|
log(`${host}\uFF1A\u6293\u5230\u4E86\u6807\u9898\uFF0C\u4F46\u8BB0\u5F55\u5DF2\u7ECF\u6709\u540D\u5B57\u4E86`);
|
|
@@ -295,19 +310,71 @@ async function miss(vault, id, reason) {
|
|
|
295
310
|
|
|
296
311
|
// src/host/classify/rules.ts
|
|
297
312
|
var PLATFORMS = [
|
|
298
|
-
|
|
299
|
-
["
|
|
300
|
-
["
|
|
301
|
-
["
|
|
302
|
-
["
|
|
303
|
-
["
|
|
304
|
-
["
|
|
305
|
-
["
|
|
313
|
+
// 视频 / 音频
|
|
314
|
+
["bilibili.com", "bilibili", "media"],
|
|
315
|
+
["b23.tv", "bilibili", "media"],
|
|
316
|
+
["youtube.com", "youtube", "media"],
|
|
317
|
+
["youtu.be", "youtube", "media"],
|
|
318
|
+
["vimeo.com", "vimeo", "media"],
|
|
319
|
+
["youku.com", "youku", "media"],
|
|
320
|
+
["v.qq.com", "tencentvideo", "media"],
|
|
321
|
+
["iqiyi.com", "iqiyi", "media"],
|
|
322
|
+
["mgtv.com", "mgtv", "media"],
|
|
323
|
+
["douyin.com", "douyin", "media"],
|
|
324
|
+
["iesdouyin.com", "douyin", "media"],
|
|
325
|
+
["kuaishou.com", "kuaishou", "media"],
|
|
326
|
+
["ixigua.com", "xigua", "media"],
|
|
327
|
+
["tiktok.com", "tiktok", "media"],
|
|
328
|
+
["twitch.tv", "twitch", "media"],
|
|
329
|
+
["dailymotion.com", "dailymotion", "media"],
|
|
330
|
+
["music.163.com", "netease-music", "media"],
|
|
331
|
+
["y.qq.com", "qq-music", "media"],
|
|
332
|
+
["spotify.com", "spotify", "media"],
|
|
333
|
+
["soundcloud.com", "soundcloud", "media"],
|
|
334
|
+
["ximalaya.com", "ximalaya", "media"],
|
|
335
|
+
// 文章 / 帖子
|
|
336
|
+
["mp.weixin.qq.com", "wechat", "article"],
|
|
337
|
+
["weixin.qq.com", "wechat", "article"],
|
|
338
|
+
["zhihu.com", "zhihu", "article"],
|
|
339
|
+
["juejin.cn", "juejin", "article"],
|
|
340
|
+
["csdn.net", "csdn", "article"],
|
|
341
|
+
["cnblogs.com", "cnblogs", "article"],
|
|
342
|
+
["jianshu.com", "jianshu", "article"],
|
|
343
|
+
["segmentfault.com", "segmentfault", "article"],
|
|
344
|
+
["v2ex.com", "v2ex", "article"],
|
|
345
|
+
["sspai.com", "sspai", "article"],
|
|
346
|
+
["36kr.com", "36kr", "article"],
|
|
347
|
+
["infoq.cn", "infoq", "article"],
|
|
348
|
+
["toutiao.com", "toutiao", "article"],
|
|
349
|
+
["weibo.com", "weibo", "article"],
|
|
350
|
+
["weibo.cn", "weibo", "article"],
|
|
351
|
+
["douban.com", "douban", "article"],
|
|
352
|
+
["xiaohongshu.com", "xiaohongshu", "article"],
|
|
353
|
+
["xhslink.com", "xiaohongshu", "article"],
|
|
354
|
+
["maimai.cn", "maimai", "article"],
|
|
355
|
+
["yuque.com", "yuque", "article"],
|
|
356
|
+
["medium.com", "medium", "article"],
|
|
357
|
+
["substack.com", "substack", "article"],
|
|
358
|
+
["dev.to", "devto", "article"],
|
|
359
|
+
["news.ycombinator.com", "hackernews", "article"],
|
|
360
|
+
["reddit.com", "reddit", "article"],
|
|
361
|
+
["stackoverflow.com", "stackoverflow", "article"],
|
|
362
|
+
["arxiv.org", "arxiv", "article"],
|
|
363
|
+
["developer.mozilla.org", "mdn", "article"],
|
|
364
|
+
["x.com", "twitter", "article"],
|
|
365
|
+
["twitter.com", "twitter", "article"],
|
|
366
|
+
["instagram.com", "instagram", "article"],
|
|
367
|
+
["threads.net", "threads", "article"],
|
|
368
|
+
["bsky.app", "bluesky", "article"],
|
|
369
|
+
["t.me", "telegram", "article"],
|
|
370
|
+
["linkedin.com", "linkedin", "article"],
|
|
371
|
+
// 代码 / 包:认得出站点,说不准"文章还是视频",交给模型
|
|
306
372
|
["github.com", "github"],
|
|
307
|
-
["
|
|
308
|
-
["
|
|
309
|
-
["
|
|
310
|
-
["
|
|
373
|
+
["gitlab.com", "gitlab"],
|
|
374
|
+
["gitee.com", "gitee"],
|
|
375
|
+
["npmjs.com", "npm"],
|
|
376
|
+
["pypi.org", "pypi"],
|
|
377
|
+
["huggingface.co", "huggingface"]
|
|
311
378
|
];
|
|
312
379
|
var SECRET_PATTERNS = [
|
|
313
380
|
[/\b(secret|access)[-_]?(id|key)\b\s*[:=]/i, "\u51FA\u73B0 secretId / secretKey \u8D4B\u503C"],
|
|
@@ -333,28 +400,28 @@ function hostOf(url) {
|
|
|
333
400
|
return void 0;
|
|
334
401
|
}
|
|
335
402
|
}
|
|
336
|
-
function
|
|
403
|
+
function ruleFor(url) {
|
|
337
404
|
const host = hostOf(url);
|
|
338
405
|
if (host === void 0) return void 0;
|
|
339
|
-
for (const
|
|
340
|
-
|
|
406
|
+
for (const rule of PLATFORMS) {
|
|
407
|
+
const [suffix] = rule;
|
|
408
|
+
if (host === suffix || host.endsWith(`.${suffix}`)) return rule;
|
|
341
409
|
}
|
|
342
410
|
return void 0;
|
|
343
411
|
}
|
|
344
|
-
function
|
|
345
|
-
|
|
346
|
-
return !path.startsWith("/read/");
|
|
347
|
-
}
|
|
348
|
-
return /\/video\/|\/watch\b|\/audio\/|\/podcast/.test(path);
|
|
412
|
+
function platformOf(url) {
|
|
413
|
+
return ruleFor(url)?.[1];
|
|
349
414
|
}
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
return /\/article\/|\/post\/|\/blog\/|\/read
|
|
415
|
+
function pathSaysMedia(path) {
|
|
416
|
+
return /\/video\/|\/watch\b|\/audio\/|\/podcast|\/shorts\/|\/v_show\/|\/playlist\b/.test(path);
|
|
417
|
+
}
|
|
418
|
+
function pathSaysArticle(path) {
|
|
419
|
+
return /\/article\/|\/articles\/|\/post\/|\/posts\/|\/blog\/|\/read\/|\/story\/|\/item\b/.test(path);
|
|
355
420
|
}
|
|
356
421
|
function classifyLink(url) {
|
|
357
|
-
const
|
|
422
|
+
const rule = ruleFor(url);
|
|
423
|
+
const platform = rule?.[1];
|
|
424
|
+
const habit = rule?.[2];
|
|
358
425
|
const parsed = (() => {
|
|
359
426
|
try {
|
|
360
427
|
return new URL(url);
|
|
@@ -365,15 +432,22 @@ function classifyLink(url) {
|
|
|
365
432
|
if (parsed === void 0) {
|
|
366
433
|
return { category: "other", confidence: "unsure", reason: "\u94FE\u63A5\u89E3\u6790\u4E0D\u4E86" };
|
|
367
434
|
}
|
|
368
|
-
const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
|
|
369
435
|
const path = parsed.pathname;
|
|
370
436
|
const base = platform === void 0 ? {} : { platform };
|
|
371
|
-
if (
|
|
437
|
+
if (pathSaysMedia(path)) {
|
|
372
438
|
return { ...base, category: "media", confidence: "decided", reason: "\u94FE\u63A5\u6307\u5411\u89C6\u9891/\u97F3\u9891\u9875" };
|
|
373
439
|
}
|
|
374
|
-
if (
|
|
440
|
+
if (pathSaysArticle(path)) {
|
|
375
441
|
return { ...base, category: "article", confidence: "decided", reason: "\u94FE\u63A5\u6307\u5411\u6587\u7AE0\u9875" };
|
|
376
442
|
}
|
|
443
|
+
if (habit !== void 0) {
|
|
444
|
+
return {
|
|
445
|
+
...base,
|
|
446
|
+
category: habit,
|
|
447
|
+
confidence: "decided",
|
|
448
|
+
reason: habit === "media" ? `${String(platform)} \u4E0A\u7684\u89C6\u9891/\u97F3\u9891\u9875` : `${String(platform)} \u4E0A\u7684\u6587\u7AE0\u9875`
|
|
449
|
+
};
|
|
450
|
+
}
|
|
377
451
|
if (platform !== void 0) {
|
|
378
452
|
return { ...base, category: "other", confidence: "unsure", reason: `\u8BA4\u5F97\u51FA\u5E73\u53F0\u662F ${platform}\uFF0C\u4F46\u8BF4\u4E0D\u51C6\u662F\u6587\u7AE0\u8FD8\u662F\u89C6\u9891` };
|
|
379
453
|
}
|
|
@@ -459,7 +533,17 @@ function mergedNote(current, incoming) {
|
|
|
459
533
|
async function absorb(vault, existing, incoming) {
|
|
460
534
|
const patched = await vault.patch(existing.id, {
|
|
461
535
|
note: mergedNote(existing, incoming.note),
|
|
462
|
-
...existing.title === void 0 && incoming.title !== void 0 ? { title: incoming.title } : {}
|
|
536
|
+
...existing.title === void 0 && incoming.title !== void 0 ? { title: incoming.title } : {},
|
|
537
|
+
/*
|
|
538
|
+
A repeat is the cheap chance to fill in what the rules learned since.
|
|
539
|
+
|
|
540
|
+
`platform` is derived from the host, and the table grows (掘金 was added
|
|
541
|
+
after a record for it already existed), so an old record can sit there
|
|
542
|
+
with no platform while a fresh paste of the same URL knows it. This only
|
|
543
|
+
ever *fills*: a record that already names a platform keeps it, because
|
|
544
|
+
that value may be what the user corrected by hand.
|
|
545
|
+
*/
|
|
546
|
+
...existing.platform === void 0 && incoming.platform !== void 0 ? { platform: incoming.platform } : {}
|
|
463
547
|
});
|
|
464
548
|
const restored = existing.deletedAt !== void 0;
|
|
465
549
|
const item = restored ? await vault.restore(existing.id) : patched;
|
|
@@ -493,7 +577,12 @@ async function captureText(vault, raw, source, note) {
|
|
|
493
577
|
const existing = vault.list({ includeDeleted: true, kinds: [sniffed.kind] }).find(
|
|
494
578
|
(item) => sniffed.kind === "link" ? item.url !== void 0 && normalizeLink(item.url) === normalizeLink(raw) : sealed === void 0 ? item.text === raw : item.secretDigest === sealed.secretDigest
|
|
495
579
|
);
|
|
496
|
-
if (existing !== void 0)
|
|
580
|
+
if (existing !== void 0) {
|
|
581
|
+
return absorb(vault, existing, {
|
|
582
|
+
note,
|
|
583
|
+
...platform === void 0 ? {} : { platform }
|
|
584
|
+
});
|
|
585
|
+
}
|
|
497
586
|
return { item: await vault.create(candidate), merged: false, restored: false, verdict };
|
|
498
587
|
}
|
|
499
588
|
async function captureImage(vault, attachment, source, note) {
|
|
@@ -1404,13 +1493,14 @@ async function mergeOnce(vault, tree, prefix, admit) {
|
|
|
1404
1493
|
const failures = [];
|
|
1405
1494
|
let merged = 0;
|
|
1406
1495
|
let added = 0;
|
|
1496
|
+
let deletions = 0;
|
|
1407
1497
|
let kept = 0;
|
|
1408
1498
|
let attachments = 0;
|
|
1409
1499
|
let objects;
|
|
1410
1500
|
try {
|
|
1411
1501
|
objects = await tree.list(`${prefix}/items/`);
|
|
1412
1502
|
} catch (error) {
|
|
1413
|
-
return { merged: 0, added: 0, kept: 0, attachments: 0, failures: [`\u5217\u8FDC\u7AEF\u540C\u6B65\u76EE\u5F55\u5931\u8D25\uFF1A${reasonOf(error)}`] };
|
|
1503
|
+
return { merged: 0, added: 0, deletions: 0, kept: 0, attachments: 0, failures: [`\u5217\u8FDC\u7AEF\u540C\u6B65\u76EE\u5F55\u5931\u8D25\uFF1A${reasonOf(error)}`] };
|
|
1414
1504
|
}
|
|
1415
1505
|
const wanted = /* @__PURE__ */ new Map();
|
|
1416
1506
|
const bytesAt = /* @__PURE__ */ new Map();
|
|
@@ -1441,6 +1531,7 @@ async function mergeOnce(vault, tree, prefix, admit) {
|
|
|
1441
1531
|
await vault.import(remote);
|
|
1442
1532
|
merged += 1;
|
|
1443
1533
|
if (local === void 0) added += 1;
|
|
1534
|
+
if (remote.deletedAt !== void 0) deletions += 1;
|
|
1444
1535
|
for (const attachmentId of remote.attachmentIds) {
|
|
1445
1536
|
if (vault.getAttachment(attachmentId) === void 0) {
|
|
1446
1537
|
wanted.set(attachmentId, remote.id);
|
|
@@ -1475,7 +1566,7 @@ async function mergeOnce(vault, tree, prefix, admit) {
|
|
|
1475
1566
|
failures.push(`\u9644\u4EF6 ${attachmentId}\uFF1A${reasonOf(error)}`);
|
|
1476
1567
|
}
|
|
1477
1568
|
}
|
|
1478
|
-
return { merged, added, kept, attachments, failures };
|
|
1569
|
+
return { merged, added, deletions, kept, attachments, failures };
|
|
1479
1570
|
}
|
|
1480
1571
|
function extensionOfName(path) {
|
|
1481
1572
|
return /\.([A-Za-z0-9]{1,8})$/.exec(nameOf2(path))?.[1]?.toLowerCase() ?? "bin";
|
|
@@ -1488,7 +1579,7 @@ async function mergeRemote(ctx, vault, attachments, extraRoots = []) {
|
|
|
1488
1579
|
const prefix = syncRoot(settings);
|
|
1489
1580
|
try {
|
|
1490
1581
|
const tree = settings.protocol === "s3" ? await s3Tree(ctx, settings) : await webdavTree(ctx, settings);
|
|
1491
|
-
if (tree === void 0) return { merged: 0, added: 0, kept: 0, attachments: 0, failures: [] };
|
|
1582
|
+
if (tree === void 0) return { merged: 0, added: 0, deletions: 0, kept: 0, attachments: 0, failures: [] };
|
|
1492
1583
|
const admit = admitWith(attachments);
|
|
1493
1584
|
const outcome = await mergeOnce(vault, tree, prefix, admit);
|
|
1494
1585
|
for (const root of extraRoots) {
|
|
@@ -1496,13 +1587,14 @@ async function mergeRemote(ctx, vault, attachments, extraRoots = []) {
|
|
|
1496
1587
|
const extra = await mergeOnce(vault, tree, root, admit);
|
|
1497
1588
|
outcome.merged += extra.merged;
|
|
1498
1589
|
outcome.added += extra.added;
|
|
1590
|
+
outcome.deletions += extra.deletions;
|
|
1499
1591
|
outcome.kept += extra.kept;
|
|
1500
1592
|
outcome.attachments += extra.attachments;
|
|
1501
1593
|
outcome.failures.push(...extra.failures);
|
|
1502
1594
|
}
|
|
1503
1595
|
return outcome;
|
|
1504
1596
|
} catch (error) {
|
|
1505
|
-
return { merged: 0, added: 0, kept: 0, attachments: 0, failures: [reasonOf(error)] };
|
|
1597
|
+
return { merged: 0, added: 0, deletions: 0, kept: 0, attachments: 0, failures: [reasonOf(error)] };
|
|
1506
1598
|
}
|
|
1507
1599
|
}
|
|
1508
1600
|
function admitWith(store) {
|
|
@@ -1609,6 +1701,7 @@ async function withMerge(ctx, vault, attachments, pulled) {
|
|
|
1609
1701
|
...pulled,
|
|
1610
1702
|
merged: outcome.merged,
|
|
1611
1703
|
added: outcome.added,
|
|
1704
|
+
deletions: outcome.deletions,
|
|
1612
1705
|
kept: outcome.kept,
|
|
1613
1706
|
attachments: outcome.attachments,
|
|
1614
1707
|
failed: pulled.failed + outcome.failures.length,
|
|
@@ -86,17 +86,6 @@ export declare function categoryLabel(category: Category): string;
|
|
|
86
86
|
export declare function sourceLabel(source: CategorySource): string;
|
|
87
87
|
/** One sentence explaining what that badge implies. */
|
|
88
88
|
export declare function sourceHint(source: CategorySource): string;
|
|
89
|
-
/**
|
|
90
|
-
* Why a link never got a name.
|
|
91
|
-
*
|
|
92
|
-
* The host stores a machine-readable code (`http:403`, `no-title`, …) — never a
|
|
93
|
-
* sentence, and deliberately not in the host's language — so the panel can say
|
|
94
|
-
* it in the reader's.
|
|
95
|
-
*
|
|
96
|
-
* @param code - the stored `linkTitleError`, or undefined when there is none.
|
|
97
|
-
* @returns a sentence, or undefined when there is nothing to explain.
|
|
98
|
-
*/
|
|
99
|
-
export declare function titleMissReason(code: string | undefined): string | undefined;
|
|
100
89
|
/**
|
|
101
90
|
* Hook the panel's copy up to dsh's language.
|
|
102
91
|
*
|
|
@@ -41,11 +41,6 @@ export declare const zh: {
|
|
|
41
41
|
'source.rule.hint': string;
|
|
42
42
|
'source.model.hint': string;
|
|
43
43
|
'source.user.hint': string;
|
|
44
|
-
'link.noTitle': string;
|
|
45
|
-
'link.http': string;
|
|
46
|
-
'link.notHtml': string;
|
|
47
|
-
'link.unreachable': string;
|
|
48
|
-
'link.unknown': string;
|
|
49
44
|
'app.settings': string;
|
|
50
45
|
'app.manual': string;
|
|
51
46
|
'app.counts': string;
|
|
@@ -98,7 +93,6 @@ export declare const zh: {
|
|
|
98
93
|
'detail.removeTag': string;
|
|
99
94
|
'detail.updated': string;
|
|
100
95
|
'detail.sealedNote': string;
|
|
101
|
-
'detail.titleMissed': string;
|
|
102
96
|
'detail.play': string;
|
|
103
97
|
'detail.zoom': string;
|
|
104
98
|
'detail.playInBrowser': string;
|
|
@@ -186,7 +180,7 @@ export declare const zh: {
|
|
|
186
180
|
'sync.detailPushed': string;
|
|
187
181
|
'sync.detailPulled': string;
|
|
188
182
|
'sync.detailAdded': string;
|
|
189
|
-
'sync.
|
|
183
|
+
'sync.detailDeleted': string;
|
|
190
184
|
'sync.pullForeignSync': string;
|
|
191
185
|
'sync.detailForeign': string;
|
|
192
186
|
'sync.shortPushed': string;
|
|
@@ -310,11 +304,6 @@ export declare const MESSAGES: {
|
|
|
310
304
|
'source.rule.hint': string;
|
|
311
305
|
'source.model.hint': string;
|
|
312
306
|
'source.user.hint': string;
|
|
313
|
-
'link.noTitle': string;
|
|
314
|
-
'link.http': string;
|
|
315
|
-
'link.notHtml': string;
|
|
316
|
-
'link.unreachable': string;
|
|
317
|
-
'link.unknown': string;
|
|
318
307
|
'app.settings': string;
|
|
319
308
|
'app.manual': string;
|
|
320
309
|
'app.counts': string;
|
|
@@ -367,7 +356,6 @@ export declare const MESSAGES: {
|
|
|
367
356
|
'detail.removeTag': string;
|
|
368
357
|
'detail.updated': string;
|
|
369
358
|
'detail.sealedNote': string;
|
|
370
|
-
'detail.titleMissed': string;
|
|
371
359
|
'detail.play': string;
|
|
372
360
|
'detail.zoom': string;
|
|
373
361
|
'detail.playInBrowser': string;
|
|
@@ -455,7 +443,7 @@ export declare const MESSAGES: {
|
|
|
455
443
|
'sync.detailPushed': string;
|
|
456
444
|
'sync.detailPulled': string;
|
|
457
445
|
'sync.detailAdded': string;
|
|
458
|
-
'sync.
|
|
446
|
+
'sync.detailDeleted': string;
|
|
459
447
|
'sync.pullForeignSync': string;
|
|
460
448
|
'sync.detailForeign': string;
|
|
461
449
|
'sync.shortPushed': string;
|
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
* private ones, follows only same-origin redirects, and caps both time and size.
|
|
9
9
|
* It runs after the record is stored, never in front of the paste.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* Four rules keep it honest:
|
|
12
12
|
* - it never writes `title`. That field is the user's own word, and the
|
|
13
13
|
* credentials rule depends on it staying that way (`AGENTS.md` 3); the fetched
|
|
14
14
|
* headline lives in `linkTitle` instead;
|
|
15
15
|
* - it writes nothing when the record already has a name, and nothing when the
|
|
16
16
|
* user renamed it while the fetch was in flight;
|
|
17
|
+
* - it does not take a name from a page that is not the page: a site can answer
|
|
18
|
+
* with an anti-bot page that carries a *real-looking* `<title>` (bilibili's is
|
|
19
|
+
* 「验证码_哔哩哔哩」), and that string names the refusal, not the link;
|
|
17
20
|
* - it never turns a failure into a problem: a dead link, a slow host or a page
|
|
18
21
|
* that is not HTML simply leaves the URL as the name, which is where this
|
|
19
22
|
* started.
|
|
@@ -34,6 +37,27 @@ export interface WebFetchSeam {
|
|
|
34
37
|
readonly url: string;
|
|
35
38
|
}, signal?: AbortSignal): Promise<WebFetchResult>;
|
|
36
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* True when this document is a refusal wearing a page's clothes.
|
|
42
|
+
*
|
|
43
|
+
* The checks before this one — a 2xx status, an HTML body, a non-empty
|
|
44
|
+
* `<title>` — all pass on bilibili's anti-bot page, which is what made a record
|
|
45
|
+
* show 「验证码」 as its name (2026-09-21). What separates that page from a real
|
|
46
|
+
* one is that it carries **no text at all**: the challenge shell is 1360 bytes
|
|
47
|
+
* of scripts and empty containers, while the page it stands in for has
|
|
48
|
+
* thousands of characters. Only then do the markers and the shape of the title
|
|
49
|
+
* decide, because the markers alone are not evidence — real bilibili pages
|
|
50
|
+
* carry the same strings.
|
|
51
|
+
*
|
|
52
|
+
* Both halves are deliberately biased towards refusing: a link whose name we
|
|
53
|
+
* skip shows its own address, which is honest and one keystroke away from being
|
|
54
|
+
* named by hand, while a refusal kept as a name is a wrong name that stays.
|
|
55
|
+
*
|
|
56
|
+
* @param html - the decoded document.
|
|
57
|
+
* @param title - the headline pulled out of it.
|
|
58
|
+
* @returns true when the page refuses to be read.
|
|
59
|
+
*/
|
|
60
|
+
export declare function looksLikeRefusal(html: string, title: string): boolean;
|
|
37
61
|
/**
|
|
38
62
|
* Pull the `<title>` out of an HTML document.
|
|
39
63
|
*
|
|
@@ -42,6 +42,15 @@ export interface SyncTree {
|
|
|
42
42
|
export interface MergeOutcome {
|
|
43
43
|
/** Records that were new here, or overwritten because the remote was newer. */
|
|
44
44
|
merged: number;
|
|
45
|
+
/**
|
|
46
|
+
* How many of {@link merged} arrived as deletions.
|
|
47
|
+
*
|
|
48
|
+
* A deleted record travels as an ordinary record with deletedAt set, so a
|
|
49
|
+
* merge that brings deletions looks exactly like one that brings updates until
|
|
50
|
+
* somebody opens the recycle bin (measured 2026-09-21: 19 merged — 6 new
|
|
51
|
+
* records and 13 tombstones, and the bin went from empty to thirteen).
|
|
52
|
+
*/
|
|
53
|
+
deletions: number;
|
|
45
54
|
/**
|
|
46
55
|
* How many of {@link merged} were ids this vault did not have at all.
|
|
47
56
|
*
|
|
@@ -175,6 +175,8 @@ export interface PullResult {
|
|
|
175
175
|
merged?: number;
|
|
176
176
|
/** How many of {@link merged} were ids this vault did not have at all. */
|
|
177
177
|
added?: number;
|
|
178
|
+
/** How many of {@link merged} were deletions (they land in the recycle bin). */
|
|
179
|
+
deletions?: number;
|
|
178
180
|
/** Attachment objects the merge had to fetch and admit locally. */
|
|
179
181
|
attachments?: number;
|
|
180
182
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chance722/dsh-inbox",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Personal paste inbox for DeepSeek Harness (dsh): capture, classify, browse and retrieve.",
|
|
6
6
|
"keywords": [
|
|
@@ -64,7 +64,10 @@
|
|
|
64
64
|
"build": "node scripts/build.mjs && tsc -p tsconfig.build.json",
|
|
65
65
|
"typecheck": "tsc --noEmit",
|
|
66
66
|
"test": "vitest run",
|
|
67
|
-
"prepublishOnly": "npm run build"
|
|
67
|
+
"prepublishOnly": "npm run build",
|
|
68
|
+
"dev:status": "node scripts/dev.mjs status",
|
|
69
|
+
"dev:local": "node scripts/dev.mjs local",
|
|
70
|
+
"dev:npm": "node scripts/dev.mjs npm"
|
|
68
71
|
},
|
|
69
72
|
"dependencies": {
|
|
70
73
|
"lucide-react": "^1.47.0",
|