shuadouyin 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dy-build-commit +1 -1
- package/dist/app.js +3 -0
- package/dist/catalog.js +15 -0
- package/dist/errors.js +24 -1
- package/dist/operations/ai-search.js +42 -0
- package/dist/operations/digest.js +70 -11
- package/dist/operations/navigation.js +9 -5
- package/dist/page-adapter.js +24 -0
- package/dist/tui.js +3 -0
- package/package.json +1 -1
- package/skills/dy/SKILL.md +26 -108
- package/skills/dy/references/browse-and-grow.md +16 -8
- package/skills/dy/references/commands.md +1 -0
package/.dy-build-commit
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
74b35764d458acaeeff44eaef3fe9e9a99941ac8
|
package/dist/app.js
CHANGED
|
@@ -4,6 +4,7 @@ import { COMMANDS } from "./catalog.js";
|
|
|
4
4
|
import { CliError, normalizeError } from "./errors.js";
|
|
5
5
|
import { Knowledge, knowledgeOperation, validateKnowledge } from "./knowledge.js";
|
|
6
6
|
import { migrateLegacyLayout } from "./migrate.js";
|
|
7
|
+
import { askDouyin } from "./operations/ai-search.js";
|
|
7
8
|
import { audio, capture, noteImage, noteSlide, observe } from "./operations/capture.js";
|
|
8
9
|
import { captureChatImage, listChats, readChat } from "./operations/chat.js";
|
|
9
10
|
import { compareChatWindows } from "./operations/chat-diff.js";
|
|
@@ -617,6 +618,8 @@ export class App {
|
|
|
617
618
|
case "user.followers":
|
|
618
619
|
case "user.following":
|
|
619
620
|
return userRelations(session, { ...input, relation: operation === "user.following" ? "following" : "followers" }, signal);
|
|
621
|
+
case "ai.ask":
|
|
622
|
+
return askDouyin(session, input, signal);
|
|
620
623
|
case "person.read":
|
|
621
624
|
input.user_id = resourceID(input.resource ?? "", "user");
|
|
622
625
|
return readPerson(session, input, signal);
|
package/dist/catalog.js
CHANGED
|
@@ -727,6 +727,21 @@ export const COMMANDS = [
|
|
|
727
727
|
description: "Move to the previous video and verify that its ID changed",
|
|
728
728
|
effect: "navigation",
|
|
729
729
|
},
|
|
730
|
+
{
|
|
731
|
+
id: "ai.ask",
|
|
732
|
+
description: "Ask Douyin's own assistant a question and read what it answers",
|
|
733
|
+
flags: [
|
|
734
|
+
{
|
|
735
|
+
key: "query",
|
|
736
|
+
name: "--query <question>",
|
|
737
|
+
description: "What to ask, up to 500 characters",
|
|
738
|
+
type: "string",
|
|
739
|
+
required: true,
|
|
740
|
+
},
|
|
741
|
+
],
|
|
742
|
+
effect: "navigation",
|
|
743
|
+
guidance: "Douyin's 抖音 AI answers in prose and often lists works with links. That is a different kind of evidence from everything else here: the rest reports what the page shows, this reports what the platform's model says about its own content. Treat the answer as a lead to check by opening the work, and quote it as the platform's claim. The work ids it links to are real page links and can be opened with video open. Answers stream in; complete=false means the read gave up before the text stopped growing.",
|
|
744
|
+
},
|
|
730
745
|
{
|
|
731
746
|
id: "search",
|
|
732
747
|
description: "Search Douyin and return loaded video results",
|
package/dist/errors.js
CHANGED
|
@@ -74,6 +74,29 @@ export function errorExit(error) {
|
|
|
74
74
|
return 1;
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
+
// Whether running the same command again could reasonably work.
|
|
78
|
+
//
|
|
79
|
+
// The field was hardcoded false, so it told a caller nothing. The distinction it
|
|
80
|
+
// should carry is the one that decides what to do next: a page that was mid-render
|
|
81
|
+
// is worth one more try, and a changed selector is not — repeating it just spends
|
|
82
|
+
// the same seconds for the same answer.
|
|
83
|
+
//
|
|
84
|
+
// `selector_changed` is deliberately in neither list. It is the one code that
|
|
85
|
+
// covers both a transition and a genuine structural change, and the page adapter
|
|
86
|
+
// already names which happened when it can ("no size", "covered", "not clickable").
|
|
87
|
+
// Marking it retryable would turn a broken page into a loop.
|
|
88
|
+
export function retryable(code) {
|
|
89
|
+
return [
|
|
90
|
+
// The browser or the network was not ready; nothing about the request was wrong.
|
|
91
|
+
"timeout",
|
|
92
|
+
"browser_not_running",
|
|
93
|
+
"browser_unavailable",
|
|
94
|
+
// Another command held the lease. It has since finished.
|
|
95
|
+
"busy",
|
|
96
|
+
// The page had not finished laying out.
|
|
97
|
+
"editor_not_ready",
|
|
98
|
+
].includes(code);
|
|
99
|
+
}
|
|
77
100
|
export function errorData(error) {
|
|
78
101
|
const problem = normalizeError(error);
|
|
79
102
|
return {
|
|
@@ -81,7 +104,7 @@ export function errorData(error) {
|
|
|
81
104
|
code: problem.code,
|
|
82
105
|
message: problem.message,
|
|
83
106
|
...(problem.hint ? { hint: problem.hint } : {}),
|
|
84
|
-
retryable:
|
|
107
|
+
retryable: retryable(problem.code),
|
|
85
108
|
...(problem.details === undefined ? {} : { details: problem.details }),
|
|
86
109
|
};
|
|
87
110
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import { CliError } from "../errors.js";
|
|
3
|
+
import { navigate } from "./navigation.js";
|
|
4
|
+
const LIMITS = "This is Douyin's assistant answering about its own content, not an observation of any work. Treat the prose as a lead to check by opening the work, and quote it as the platform's claim rather than as something this tool saw. The ids are real page links and can be opened with dy video open.";
|
|
5
|
+
export async function askDouyin(session, input, signal) {
|
|
6
|
+
const question = (input.query ?? "").trim();
|
|
7
|
+
if (!question)
|
|
8
|
+
throw new CliError("invalid_argument", "Ask something: --query <question>");
|
|
9
|
+
if (question.length > 500)
|
|
10
|
+
throw new CliError("invalid_argument", "Keep the question under 500 characters");
|
|
11
|
+
await navigate(session, "https://www.douyin.com/aisearch?from_nav=1", "agent", signal);
|
|
12
|
+
const prepared = await session.call("ai.prepare", { question });
|
|
13
|
+
if (!prepared.ready)
|
|
14
|
+
throw new CliError("selector_changed", "The AI search box did not appear");
|
|
15
|
+
await session.tab.keyboard.insertText(question);
|
|
16
|
+
await session.tab.keyboard.press("Enter");
|
|
17
|
+
// The answer streams in. Waiting for the text to stop growing is what makes the
|
|
18
|
+
// difference between a partial answer and a complete one, and this reports which.
|
|
19
|
+
const deadline = Date.now() + 45_000;
|
|
20
|
+
let previous = "";
|
|
21
|
+
let stable = 0;
|
|
22
|
+
let answer = null;
|
|
23
|
+
while (Date.now() < deadline) {
|
|
24
|
+
signal.throwIfAborted();
|
|
25
|
+
answer = await session.call("ai.answer", { question });
|
|
26
|
+
if (answer.answer === previous && answer.answer.length > 0)
|
|
27
|
+
stable++;
|
|
28
|
+
else
|
|
29
|
+
stable = 0;
|
|
30
|
+
previous = answer.answer;
|
|
31
|
+
// Two reads in a row with the same text means the stream finished.
|
|
32
|
+
if (stable >= 2)
|
|
33
|
+
return { ...answer, complete: true, limits: LIMITS };
|
|
34
|
+
await delay(600, undefined, { signal });
|
|
35
|
+
}
|
|
36
|
+
if (!answer || !answer.answer)
|
|
37
|
+
throw new CliError("timeout", "The AI search page did not answer", {
|
|
38
|
+
hint: "The question may have been refused, or the page changed. Read it with dy page facts.",
|
|
39
|
+
});
|
|
40
|
+
return { ...answer, complete: false, limits: LIMITS };
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=ai-search.js.map
|
|
@@ -2,25 +2,47 @@ import { join } from "node:path";
|
|
|
2
2
|
import { setTimeout as delay } from "node:timers/promises";
|
|
3
3
|
import { classifyComment } from "../comment-stance.js";
|
|
4
4
|
import { CliError } from "../errors.js";
|
|
5
|
-
import { captureSnapshot } from "./capture.js";
|
|
5
|
+
import { captureSnapshot, noteImage, noteSlide } from "./capture.js";
|
|
6
6
|
import { listComments } from "./comments.js";
|
|
7
7
|
// Long works earn more samples; a nine-second clip is its own summary. The
|
|
8
8
|
// fractions avoid the opening (usually a title card) and the end (usually a
|
|
9
9
|
// call to follow), which are the two least informative parts of a short video.
|
|
10
|
+
// How many samples a work deserves, from its length.
|
|
11
|
+
//
|
|
12
|
+
// Three frames is right for a few minutes and much too few for twenty: half an hour
|
|
13
|
+
// sampled at 25/50/75% is three stills of somebody talking, and the argument they
|
|
14
|
+
// made in between is gone. A long work gets more, capped so the capture does not
|
|
15
|
+
// take longer than watching it.
|
|
16
|
+
export function samplesFor(duration, ceiling = 8) {
|
|
17
|
+
if (!Number.isFinite(duration) || duration <= 0)
|
|
18
|
+
return 0;
|
|
19
|
+
if (duration < 15)
|
|
20
|
+
return 0;
|
|
21
|
+
if (duration < 45)
|
|
22
|
+
return 1;
|
|
23
|
+
// One sample per two minutes past the first, plus the three the base case needs:
|
|
24
|
+
// 3 minutes gets 3, 10 minutes gets 5, 20 minutes gets 8.
|
|
25
|
+
if (duration < 180)
|
|
26
|
+
return 3;
|
|
27
|
+
return Math.min(ceiling, 3 + Math.floor((duration - 180) / 120));
|
|
28
|
+
}
|
|
10
29
|
export function samplePositions(duration, wanted) {
|
|
11
30
|
if (!Number.isFinite(duration) || duration <= 0)
|
|
12
31
|
return [];
|
|
13
|
-
|
|
14
|
-
if (
|
|
32
|
+
const automatic = samplesFor(duration);
|
|
33
|
+
if (automatic === 0)
|
|
15
34
|
return [];
|
|
16
|
-
//
|
|
17
|
-
// the middle
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
// An explicit count is honoured: `--frames 1` means one frame, and a caller that
|
|
36
|
+
// wants the middle of a long talk has a reason. What is not honoured is asking
|
|
37
|
+
// for nothing, which the hasValue check upstream already refuses.
|
|
38
|
+
const count = Math.max(wanted ?? automatic, 1);
|
|
39
|
+
// Until the first millisecond and after the last are both wrong: the opening is a
|
|
40
|
+
// title card and the end is a request to follow.
|
|
41
|
+
if (count === 1)
|
|
21
42
|
return [duration * 0.5];
|
|
22
|
-
|
|
23
|
-
|
|
43
|
+
// Spread across the middle eighty percent, so the first sample is past the
|
|
44
|
+
// opening and the last is before the sign-off.
|
|
45
|
+
return Array.from({ length: count }, (_, index) => duration * (0.1 + (0.8 * index) / (count - 1)));
|
|
24
46
|
}
|
|
25
47
|
export async function digest(session, store, input, signal) {
|
|
26
48
|
const initial = await session.call("video.info", input);
|
|
@@ -32,7 +54,44 @@ export async function digest(session, store, input, signal) {
|
|
|
32
54
|
const frames = [];
|
|
33
55
|
const limitations = [];
|
|
34
56
|
const directory = store.path(join("captures", `digest-${work.id}`));
|
|
35
|
-
|
|
57
|
+
// An image post is a set of pictures, and the set is the work. Sampling a video
|
|
58
|
+
// means picking moments inside one; sampling an image post means looking at every
|
|
59
|
+
// one of them, because the point of the format is that you swipe through them.
|
|
60
|
+
if (input.frames !== 0 && initial.note) {
|
|
61
|
+
const slides = [];
|
|
62
|
+
// Until the count is known there is nothing to loop over, so each step asks
|
|
63
|
+
// where it is and stops when the page says it is at the end.
|
|
64
|
+
for (let guard = 0; guard < (input.limit ?? 20); guard++) {
|
|
65
|
+
signal.throwIfAborted();
|
|
66
|
+
try {
|
|
67
|
+
const captured = await noteImage(session, store, { ...input, output: join(directory, `slide-${String(slides.length + 1).padStart(2, "0")}.png`) }, signal);
|
|
68
|
+
slides.push(slides.length + 1);
|
|
69
|
+
if (captured.slide.index !== null &&
|
|
70
|
+
captured.slide.count !== null &&
|
|
71
|
+
captured.slide.index + 1 >= captured.slide.count)
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
// The first page failing is worth reporting; one later page failing is
|
|
76
|
+
// the boundary, and the pages already read are still the answer.
|
|
77
|
+
if (!slides.length)
|
|
78
|
+
limitations.push(error instanceof CliError ? error.message : "Could not read the image post");
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const moved = await noteSlide(session, input, "next", signal);
|
|
83
|
+
if (moved.boundary || !moved.moved)
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// No further page could be reached. What was read stands.
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (slides.length)
|
|
92
|
+
limitations.push("Every page of the image post was captured in order.");
|
|
93
|
+
}
|
|
94
|
+
else if (input.frames !== 0 && initial.video) {
|
|
36
95
|
const positions = samplePositions(duration ?? 0, input.frames ?? 3);
|
|
37
96
|
if (!positions.length)
|
|
38
97
|
limitations.push(duration ? `${Math.round(duration)}s is short enough to read in one pass` : "This work reports no duration");
|
|
@@ -38,11 +38,15 @@ export async function navigate(session, url, kind, signal) {
|
|
|
38
38
|
return waitSnapshot(session, (state) => kind === "video"
|
|
39
39
|
? // Douyin redirects an image post from /video/ to /note/.
|
|
40
40
|
!!(state.video?.id ?? state.note?.id)
|
|
41
|
-
: kind === "
|
|
42
|
-
?
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
: kind === "agent"
|
|
42
|
+
? // The AI page reports itself as an ordinary page; what identifies it is
|
|
43
|
+
// the route and a composer being present.
|
|
44
|
+
!!state.url?.includes("/aisearch")
|
|
45
|
+
: kind === "search"
|
|
46
|
+
? state.page_kind === "search"
|
|
47
|
+
: // The route commits before the profile renders, so the URL alone would let
|
|
48
|
+
// the caller read a page whose header has not appeared yet.
|
|
49
|
+
!!state.url?.includes("/user/") && state.page_kind === "profile", signal).catch((error) => {
|
|
46
50
|
// A profile that redirects elsewhere is a real answer, not a changed selector:
|
|
47
51
|
// Douyin sends restricted or unavailable accounts to the search page. Saying
|
|
48
52
|
// "the page did not reach the expected state" hides which of the two happened
|
package/dist/page-adapter.js
CHANGED
|
@@ -3066,6 +3066,30 @@ var DouyinPage = (() => {
|
|
|
3066
3066
|
);
|
|
3067
3067
|
return { present, comment_id: args.comment_id };
|
|
3068
3068
|
}
|
|
3069
|
+
case "ai.prepare": {
|
|
3070
|
+
const editor = [...document.querySelectorAll(".douyin-chat-editor,[contenteditable='true']")].find(
|
|
3071
|
+
(element) => {
|
|
3072
|
+
const box = element.getBoundingClientRect();
|
|
3073
|
+
return box.width > 100 && box.height > 10;
|
|
3074
|
+
}
|
|
3075
|
+
);
|
|
3076
|
+
if (!editor) return { ready: false };
|
|
3077
|
+
editor.focus();
|
|
3078
|
+
return { ready: true };
|
|
3079
|
+
}
|
|
3080
|
+
case "ai.answer": {
|
|
3081
|
+
const asked = String(args.question ?? "").trim();
|
|
3082
|
+
const body = document.body.innerText ?? "";
|
|
3083
|
+
const at = asked ? body.indexOf(asked) : -1;
|
|
3084
|
+
const answer = at >= 0 ? body.slice(at + asked.length).trim() : body.trim();
|
|
3085
|
+
const links = [...document.querySelectorAll('a[href*="/video/"]')].map((anchor) => {
|
|
3086
|
+
const id = /\/video\/(\d+)/.exec(anchor.getAttribute("href") ?? "")?.[1];
|
|
3087
|
+
return id ? { id, text: clean(anchor.textContent).slice(0, 80) } : null;
|
|
3088
|
+
}).filter((entry) => entry !== null);
|
|
3089
|
+
const unique = /* @__PURE__ */ new Map();
|
|
3090
|
+
for (const link of links) if (!unique.has(link.id)) unique.set(link.id, link);
|
|
3091
|
+
return { question: asked, answer, links: [...unique.values()] };
|
|
3092
|
+
}
|
|
3069
3093
|
case "comment.writable": {
|
|
3070
3094
|
const composer = document.querySelector(".comment-input-container,.comment-input-inner-container") ?? document.querySelector(".public-DraftEditor-content");
|
|
3071
3095
|
if (!composer) {
|
package/dist/tui.js
CHANGED
|
@@ -153,6 +153,7 @@ export class TerminalUI {
|
|
|
153
153
|
{ value: "live", label: "直播", description: "读直播间、发弹幕、送礼" },
|
|
154
154
|
{ value: "social", label: "关系", description: "目标、候选、卡片和结果" },
|
|
155
155
|
{ value: "person", label: "看一个人", description: "对方发过什么、写过什么" },
|
|
156
|
+
{ value: "ai", label: "问抖音 AI", description: "让平台自己回答,当线索用" },
|
|
156
157
|
{ value: "browse", label: "浏览轮次", description: "开一轮、记看过和划过的、收尾" },
|
|
157
158
|
{ value: "note", label: "图文作品", description: "读图文帖的每一页" },
|
|
158
159
|
{ value: "link", label: "解析链接", description: "把分享文案或链接解析成作品 ID" },
|
|
@@ -189,6 +190,8 @@ export class TerminalUI {
|
|
|
189
190
|
commands = COMMANDS.filter((command) => /^social\./.test(command.id));
|
|
190
191
|
if (selected === "person")
|
|
191
192
|
commands = COMMANDS.filter((command) => /^person\./.test(command.id));
|
|
193
|
+
if (selected === "ai")
|
|
194
|
+
commands = COMMANDS.filter((command) => /^ai\./.test(command.id));
|
|
192
195
|
if (selected === "browse")
|
|
193
196
|
commands = COMMANDS.filter((command) => /^browse\./.test(command.id));
|
|
194
197
|
if (selected === "note")
|
package/package.json
CHANGED
package/skills/dy/SKILL.md
CHANGED
|
@@ -8,6 +8,10 @@ allowed-tools: Bash(dy:*) Bash(npx dy:*)
|
|
|
8
8
|
|
|
9
9
|
`dy` 是本地能力工具。当前 agent 负责理解、判断、对话、总结与调度。用 `dy schema` 查看可用命令;尊重用户当前授权和已有账号状态。
|
|
10
10
|
|
|
11
|
+
**这份文件是常驻的,所以只放每次都用的东西。** 完整的命令说明、平台事实、各场景流程在 `dy guide` 里,它的内容和你的 `dy` 版本永远一致——本文件可能落后于一次更新。拿不准某个命令怎么用,先跑 `dy guide`,别猜。
|
|
12
|
+
|
|
13
|
+
下面的命令列表够日常使用。遇到没列到的、或者行为和这里写的不一样,以 `dy guide` 和实际返回为准。
|
|
14
|
+
|
|
11
15
|
## 第一次用:先问人设
|
|
12
16
|
|
|
13
17
|
新工作区里人格还是模板。这时候每个页面命令的返回里都会带一个 `setup` 字段:
|
|
@@ -65,123 +69,37 @@ dy video info # 读当前作品
|
|
|
65
69
|
|
|
66
70
|
## 命令
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
### 刷与看
|
|
71
|
-
|
|
72
|
-
```bash
|
|
73
|
-
dy feed open # 打开推荐流
|
|
74
|
-
dy feed next # 下一条;核实 ID 变了才算成功
|
|
75
|
-
dy feed previous
|
|
76
|
-
dy feed sweep --count 10 # 一次走 10 条,返回 id/作者/标题/时长
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
**`feed sweep` 是读一批的首选。** 一条命令、一次连接,`stopped` 说明为什么停下(`count_reached` / `steps_exhausted` / `feed_stuck`)。它不快多少(瓶颈是页面过渡,一次几秒),但一次拿全数据,不用逐条 `video info`。
|
|
80
|
-
|
|
81
|
-
### 判断一条作品在讲什么
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
dy video digest # 标题 + 三个位置的画面 + 评论区摘要
|
|
85
|
-
dy video digest --frames 0 # 只读评论,不采样
|
|
86
|
-
dy video seek --at 120 # 跳到第 120 秒
|
|
87
|
-
dy video observe --at 200 --seconds 4 --frames 2
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
`digest` 约 3–4 秒。三个位置是 25%/50%/75%,避开开头封面语和结尾求关注。实测一条 4 分钟英语视频,三帧字幕拼起来就是完整观点,比标题信息量大得多。
|
|
91
|
-
|
|
92
|
-
`--frames 0` 只对 `digest` 有效,`observe` 至少要 1 帧。15 秒以下的作品不采样:`frames` 为空并附一条限制说明,那是设计如此,不是失败。`comments.count` 只数页面已加载的,同一条作品连读两次可能不一样。
|
|
93
|
-
|
|
94
|
-
### 读作品与评论
|
|
72
|
+
日常这一组就够。参数细节、每个命令的坑、各场景的完整流程都在 `dy guide`。
|
|
95
73
|
|
|
96
74
|
```bash
|
|
97
|
-
|
|
75
|
+
# 刷与看
|
|
76
|
+
dy feed sweep --count 8 # 一次读一批:id/作者/标题/时长,首选
|
|
77
|
+
dy feed next / previous
|
|
78
|
+
dy video digest # 标题 + 采样画面 + 评论摘要;图集会逐页拍
|
|
98
79
|
dy video open <id-or-url>
|
|
99
|
-
dy comment list --limit 20 # 每条带 stance 和 likes
|
|
100
|
-
dy comment replies --comment-id <id>
|
|
101
|
-
dy comment delete --comment-id <id> --confirm # 删自己发的一条,不可撤销
|
|
102
|
-
dy page scroll --target comments --pixels 600
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
`comment list` 每条带 `stance`(`about_work` / `about_self` / `asking` / `spam` / `unknown`)和 `likes`。回之前看 stance,对着「在说自己」的评论夸「唱得好」是错位的。
|
|
106
|
-
|
|
107
|
-
**它自己开的面板会自己关**,所以读完能直接 `feed next`。面板挡着翻页箭头时用 `dy comment close`。
|
|
108
|
-
|
|
109
|
-
`comment delete` 只删自己的评论,别人的 id 会直接拒绝。**页面不会问你第二次**,点了就没,所以 `--confirm` 是唯一的闸。
|
|
110
|
-
|
|
111
|
-
### 学这个区的写法
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
dy comment voice --limit 20 # 中位长度、感叹号习惯、称呼、人称、提问比例
|
|
115
|
-
dy comment voice --save # 连同句式一起存下来
|
|
116
|
-
dy comment voice --known # 已经学过的区,按形状归并
|
|
117
|
-
dy comment phrases # 哪些句式还能用
|
|
118
|
-
dy comment phrases --use "…" # 记一次使用
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
**按作品算,不按全局算。** 同一个号上,不同区的评论中位长度可以差好几倍:一个区可能七八个字,另一个区要写满四五十个字。所以别把上个区学到的语气带到这个区,每换一条作品重新量。
|
|
122
80
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
`comment voice` 还给每条评论判 `has-wit` / `flat` / `generic-praise`,依据是接话、具体指认、反转三个维度,以及堆砌术语。实测一个区里 247 赞的是「下次一起唱」,150 字堆术语那条只有 2 赞,长度和赞数相关 -0.38。
|
|
126
|
-
|
|
127
|
-
**句式用三次会自动标成 `spent`**,静默 14 天回来。别反复用同一条。
|
|
128
|
-
|
|
129
|
-
### 写
|
|
130
|
-
|
|
131
|
-
```bash
|
|
81
|
+
# 评论
|
|
82
|
+
dy comment list --limit 20 # 每条带 stance 和 likes
|
|
132
83
|
dy comment post --video-id <id> --text "…" --request-id <唯一ID>
|
|
133
84
|
dy comment reply --video-id <id> --comment-id <id> --text "…" --request-id <唯一ID>
|
|
134
|
-
dy
|
|
135
|
-
dy
|
|
136
|
-
dy
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
dy
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
```bash
|
|
148
|
-
dy live info # 主播、开播状态、能否评论
|
|
149
|
-
dy live chat --limit 20 # 当前加载的弹幕
|
|
150
|
-
dy live say --text "…" --request-id <id>
|
|
151
|
-
dy live gifts # 礼物和价格
|
|
152
|
-
dy live gift --name 人气票 --price 1 --request-id <id>
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
礼物会花真钱,`--name` 和 `--price` 必须同时匹配。
|
|
156
|
-
|
|
157
|
-
### 记忆
|
|
158
|
-
|
|
159
|
-
```bash
|
|
160
|
-
dy memory add --text "…" --source "user:…" --layer … --certainty …
|
|
161
|
-
dy memory search <词>
|
|
162
|
-
dy memory list
|
|
163
|
-
dy memory candidates --text-file <文件>
|
|
164
|
-
dy memory decide --text-file <文件> --apply --request-id <id>
|
|
85
|
+
dy comment delete --comment-id <id> --confirm
|
|
86
|
+
dy comment voice # 这个区的写法:中位长度、标点、称呼
|
|
87
|
+
dy voice check --text "…" # 发之前自查像不像机器写的
|
|
88
|
+
|
|
89
|
+
# 私信与关系
|
|
90
|
+
dy chat list / read / send
|
|
91
|
+
dy person read <id> # 这个人发过什么、写过什么
|
|
92
|
+
dy social response --conversation-id <id> # 我发的有没有人回
|
|
93
|
+
dy social advise <person> --conversation-id <id> # 继续、等、还是停
|
|
94
|
+
|
|
95
|
+
# 其他
|
|
96
|
+
dy live info / chat / say
|
|
97
|
+
dy memory add / search
|
|
165
98
|
dy context show --query "…"
|
|
99
|
+
dy ai ask --query "…" # 问平台自己的 AI,当线索用
|
|
166
100
|
```
|
|
167
101
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
```bash
|
|
171
|
-
dy init # 建工作区
|
|
172
|
-
dy onboarding show # 读引导状态
|
|
173
|
-
dy auth login # 扫码登录(有头 Chrome)
|
|
174
|
-
dy auth status
|
|
175
|
-
dy browser start / stop / status
|
|
176
|
-
dy browser clean # 回收磁盘:缓存和截图,不碰登录
|
|
177
|
-
dy diagnostics # 反复失败的命令
|
|
178
|
-
dy page facts # 页面此刻真实的状态
|
|
179
|
-
dy update --check # 有没有新版本
|
|
180
|
-
dy skill install # 装 skill 到 agent 目录
|
|
181
|
-
dy guide # 这份文档的完整内容
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
`dy source` 只对开发 dy 的人有意义,正常使用用不到。
|
|
102
|
+
**写入必须有 `--request-id`。** 加 `--progress` 看每步走到哪,`dispatching` 之后不可逆。
|
|
185
103
|
|
|
186
104
|
## 按任务读参考
|
|
187
105
|
|
|
@@ -20,14 +20,22 @@ dy video digest
|
|
|
20
20
|
|
|
21
21
|
**按长度分档:**
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
采样数跟着时长走,不是固定三帧:
|
|
24
|
+
|
|
25
|
+
| 时长 | 帧数 | 说明 |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| < 15s | 0 | 短内容本身就是全部,`frames: []` 配 `limitations` 说明原因 |
|
|
28
|
+
| 15–45s | 1 | 取正中 |
|
|
29
|
+
| 45s–3min | 3 | 分布在中间 80% |
|
|
30
|
+
| > 3min | 每 2 分钟 +1,上限 8 | 20 分钟的访谈取 8 帧 |
|
|
31
|
+
|
|
32
|
+
**不要用三帧衡量长视频。** 半小时的演讲取三点,等于只看开场、中间一瞥、结尾,论证全丢。既然命令已经按长度算好了,读 `frames` 的长度就知道有没有看够。
|
|
33
|
+
|
|
34
|
+
**`--frames N` 可以压过自动值**,但最低 1 帧。只想看一个位置就传 1。
|
|
35
|
+
|
|
36
|
+
**图集是另一回事。** `digest` 遇到图片帖会逐页截图,一页都不少,因为整条作品就是那组图。
|
|
37
|
+
|
|
38
|
+
**短作品没有帧不等于没看。** `frames: []` 配上「< 15s」的限制说明是设计如此。别当成采样失败去重试。
|
|
31
39
|
|
|
32
40
|
**论坛区比标题更接近真相。** 一条手势舞标题是「我的世界你就是那中心点」,评论区里作者在问「这一版萌还是上一版萌」——那是连载作品,标题没说。看 `stance` 判断观众在说什么:`about_work` 才是在聊作品,`about_self` 是在聊自己。
|
|
33
41
|
|
|
@@ -69,6 +69,7 @@ dy page scroll --pixels 1200
|
|
|
69
69
|
| 分享文本或链接转 ID | `dy link resolve <text>` |
|
|
70
70
|
| 读当前作品 | `dy video info` |
|
|
71
71
|
| 搜索 | `dy search <词>`、`dy user search <词>` |
|
|
72
|
+
| 问平台自己的 AI | `dy ai ask --query "…"` |
|
|
72
73
|
| 当前标签页是什么 | `dy status` |
|
|
73
74
|
|
|
74
75
|
`feed.next` 会核实作品 ID 变了才算成功。读到「当前是直播」不代表命令坏了——见 `platform-facts.md` 的滑片过渡说明。
|