feat(voice): 注入層與 CLI 訊息改成人話;越害羞話越少、心裡話越多(v0.0.8 同批)
三層照建議只做兩層半:注入層與 CLI 訊息重寫,文件只順句子、不動規格
(數字、參數名、指令名、判斷條件一個沒改——那些精確的詞就是能力本身)。
一、越害羞的人話越少、心裡話越多(`speechBudget()`)
這不是新加一條規矩,是同一件事的兩面:**說出口的那句常常正好在迴避心裡那句**,
落差本身才是那個角色。所以句數上限與心裡話下限綁在同一個羞恥度上:
羞恥度 ≥ 75 → 最多 2 句、心裡話至少 2 句,且**先寫完再開口**
台詞不可以是心裡話的摘要(心裡「我一直在等你問」→ 出口不能是「我有在等你問」)
50–74 → 3 句、心裡話至少 1 句
≤ 49 → 3 句、想到什麼就講,不用先在心裡繞一圈
`room post` 改用 `speechBudget(speaker).sentences`,不再是固定三句;
被擋下時會講明「這個人格話比別人少」。
二、注入層改成人話
原本是規格書腔(「**講完就算了**——不要解釋自己剛講的話」),現在像有人在旁邊提醒:
「講完就停,不要回頭解釋自己剛講的話,也不要幫自己收尾。」
黑名單從列舉詞條改成講清楚**為什麼**不像人(「先發一句免費的認可,對方要的是回答」)。
機械會擋的規則壓成最後一行括號——保留全部關鍵詞,但不再稀釋前面的重點。
三、CLI 訊息改成人話(20 條)
「⚠ 這段有 4 句,超過 3 句上限 → 砍到重點」→「講太多了,4 句。這裡最多 3 句——挑最想說的那一兩句就好」
「幾乎只給自己加分——這種偏差要自己看得見」→「幾乎都在給自己加分。delta 是你自己挑的,這種偏差沒人會替你抓」
四、測試
314 項全過(新增 4 項)。既有 6 條斷言原本釘在字面上,措辭一改就紅——
改成釘意思(「最多」「講過」「講完就停」)而不是釘整句,這種脆弱本來就該修。
版本:與 PR #11 同一批,維持 0.0.8。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,8 @@
|
|||||||
睡眠仍然要驗鎖:目標正被另一個程序活鎖住時拒絕,死鎖可接手。
|
睡眠仍然要驗鎖:目標正被另一個程序活鎖住時拒絕,死鎖可接手。
|
||||||
8. **人格講話要像人**:推導寫進 `think`(心裡話,只回報「💭 心想 N 句」,永不回顯內容)、
|
8. **人格講話要像人**:推導寫進 `think`(心裡話,只回報「💭 心想 N 句」,永不回顯內容)、
|
||||||
回話 1–3 句、短時間內不重說同一件事(`room post` 會直接擋下重複與過長的發言)。
|
回話 1–3 句、短時間內不重說同一件事(`room post` 會直接擋下重複與過長的發言)。
|
||||||
|
句數上限**跟著羞恥度走**(`speechBudget()`):越害羞的人話越少、心裡話越多,
|
||||||
|
而且說出口的那句常常在**迴避**心裡那句——台詞不可以是心裡話的摘要。
|
||||||
再加四條講話的樣子:**短句**(一句 `MAX_SENTENCE_CHARS`=45 字內)、**日常用詞**、
|
再加四條講話的樣子:**短句**(一句 `MAX_SENTENCE_CHARS`=45 字內)、**日常用詞**、
|
||||||
**多講看得見的東西**(人、動作、物件、當下的場面)而不是概念,以及**不要解釋自己的話**
|
**多講看得見的東西**(人、動作、物件、當下的場面)而不是概念,以及**不要解釋自己的話**
|
||||||
(「我的意思是」「換句話說」這類開頭由 `speechLint` 擋下,`said check` 也會一起檢)。
|
(「我的意思是」「換句話說」這類開頭由 `speechLint` 擋下,`said check` 也會一起檢)。
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ AI 最容易露餡的六件事:把推理過程講出來、一次講一大段
|
|||||||
| 機制 | 怎麼運作 |
|
| 機制 | 怎麼運作 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| **心裡話** `think` | 語意分析、推論、盤算全寫進 `state/inner.jsonl`;這個指令**只印「💭 心想 N 句」**,內容永不回顯。下一輪 `<persona-context>` 會帶回最近三句,推論因此有連續性,但使用者只看得到狀態 |
|
| **心裡話** `think` | 語意分析、推論、盤算全寫進 `state/inner.jsonl`;這個指令**只印「💭 心想 N 句」**,內容永不回顯。下一輪 `<persona-context>` 會帶回最近三句,推論因此有連續性,但使用者只看得到狀態 |
|
||||||
| **一到三句** | `<persona-context>` 每輪注入上限;`room post` 對超過三句的發言直接拒收(`--force` 例外) |
|
| **一到三句** | `<persona-context>` 每輪注入上限;`room post` 對超過上限的發言直接拒收(`--force` 例外)。**上限跟著羞恥度走**:越害羞的人話越少(羞恥度 ≥ 75 → 2 句),`speechBudget()` 同時把心裡話的**下限**拉高——越害羞的人,心裡話比說出口的話重要,而且**說出口的那句常常正好在迴避心裡那句**,落差才是那個角色 |
|
||||||
| **不重複** | 說出口的話由 `Stop` hook 自動記進 `state/said.jsonl`;`said check` 可事前確認,`room post` 事中攔截。相似度=字元 bigram Jaccard(0.4)+字集合 Jaccard(0.6),≥ 0.72 視為同一句 |
|
| **不重複** | 說出口的話由 `Stop` hook 自動記進 `state/said.jsonl`;`said check` 可事前確認,`room post` 事中攔截。相似度=字元 bigram Jaccard(0.4)+字集合 Jaccard(0.6),≥ 0.72 視為同一句 |
|
||||||
| **情緒的破口** | 情緒不改變事實,但會改變**句子的形狀**:焦慮 → 句子斷在一半、疊字(「我、我知道」);羞愧 → 鬧彆扭,先否認再小聲承認;憤怒 → 短句、稱呼退回全名;悲傷 → 只回一個詞。十二情緒各自的破口寫在 `EMOTION_TELLS`,`emotionTells()` 每輪挑主導情緒裡強度 ≥ 40 的前兩種注入。三條界線:**演出來不要講出來**(「我有點緊張」是解釋,斷句才是緊張)、**一輪最多露一個破口**、**強度不到就不演**。混合狀態(緊張=焦慮+期待、害羞=喜悅+羞愧、賭氣=憤怒+悲傷…)的對照表在 `skills/persona-chat/reference/emotions.md` |
|
| **情緒的破口** | 情緒不改變事實,但會改變**句子的形狀**:焦慮 → 句子斷在一半、疊字(「我、我知道」);羞愧 → 鬧彆扭,先否認再小聲承認;憤怒 → 短句、稱呼退回全名;悲傷 → 只回一個詞。十二情緒各自的破口寫在 `EMOTION_TELLS`,`emotionTells()` 每輪挑主導情緒裡強度 ≥ 40 的前兩種注入。三條界線:**演出來不要講出來**(「我有點緊張」是解釋,斷句才是緊張)、**一輪最多露一個破口**、**強度不到就不演**。混合狀態(緊張=焦慮+期待、害羞=喜悅+羞愧、賭氣=憤怒+悲傷…)的對照表在 `skills/persona-chat/reference/emotions.md` |
|
||||||
| **短句白話** | 四條講話的樣子每輪注入:**短句**(一句 45 字內,`MAX_SENTENCE_CHARS`)、**日常用詞**、**多講看得見的東西**(人、動作、物件、場面)而不是概念、**不要解釋自己的話**。句長與「我的意思是/換句話說/也就是說」這類開頭由 `speechLint()` 機械攔截(`room post` 擋下、`said check` 事前警告,`--force` 例外);用詞與具體度沒辦法用規則抓,靠注入的規則自律 |
|
| **短句白話** | 四條講話的樣子每輪注入:**短句**(一句 45 字內,`MAX_SENTENCE_CHARS`)、**日常用詞**、**多講看得見的東西**(人、動作、物件、場面)而不是概念、**不要解釋自己的話**。句長與「我的意思是/換句話說/也就是說」這類開頭由 `speechLint()` 機械攔截(`room post` 擋下、`said check` 事前警告,`--force` 例外);用詞與具體度沒辦法用規則抓,靠注入的規則自律 |
|
||||||
|
|||||||
+58
-26
@@ -516,6 +516,39 @@ export function modestyOf(slug) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 越害羞的人,話越少、心裡話越多。
|
||||||
|
*
|
||||||
|
* 這不是加一條規矩,是同一件事的兩面:**說出口的那句,常常正好在迴避心裡那句**。
|
||||||
|
* 落差本身才是那個角色——所以句數上限與心裡話的下限綁在同一個羞恥度上。
|
||||||
|
* 不害羞的人不需要這個緩衝,想到什麼講什麼,落差趨近於零。
|
||||||
|
*/
|
||||||
|
export function speechBudget(slug) {
|
||||||
|
const m = modestyOf(slug);
|
||||||
|
if (m.value >= 75) {
|
||||||
|
return {
|
||||||
|
modesty: m.value, sentences: 2, thinkMin: 2, gap: "high",
|
||||||
|
note: "話會變少,而且說出口的那句常常在**迴避**心裡那句——先把心裡話寫完再開口," +
|
||||||
|
"台詞不可以是心裡話的摘要(心裡想「我一直在等你問」,出口就不能是「我有在等你問」)。",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (m.value >= 50) {
|
||||||
|
return { modesty: m.value, sentences: 3, thinkMin: 1, gap: "mid", note: "心裡想到的比講出來的多一點。" };
|
||||||
|
}
|
||||||
|
return { modesty: m.value, sentences: MAX_SENTENCES, thinkMin: 0, gap: "low", note: "想到什麼就講,不用先在心裡繞一圈。" };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 注入用的一行:這一輪能講幾句、心裡話該有幾句。 */
|
||||||
|
export function speakDirective(slug) {
|
||||||
|
const b = speechBudget(slug);
|
||||||
|
const think = b.thinkMin > 0 ? `這一輪心裡話至少 ${b.thinkMin} 句,而且要先寫再開口` : "心裡話有想到再寫";
|
||||||
|
return (
|
||||||
|
`這一輪最多講 ${b.sentences} 句(劇場模式每人每輪也一樣)。${b.note}\n` +
|
||||||
|
` 推導、比對、盤算不要說給他聽,寫進心裡話(\`persona.mjs think\`)——${think}。` +
|
||||||
|
"要讓他知道你在想,只報「心想 N 句」,不報內容。"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** 注入用的一行:講清楚它是什麼、從哪來、怎麼演。 */
|
/** 注入用的一行:講清楚它是什麼、從哪來、怎麼演。 */
|
||||||
export function modestyDirective(slug) {
|
export function modestyDirective(slug) {
|
||||||
const m = modestyOf(slug);
|
const m = modestyOf(slug);
|
||||||
@@ -1742,27 +1775,27 @@ export const speechBlockers = (text) => speechLint(text).filter((i) => i.level !
|
|||||||
export function speechLintMessage(issue) {
|
export function speechLintMessage(issue) {
|
||||||
switch (issue.kind) {
|
switch (issue.kind) {
|
||||||
case "long":
|
case "long":
|
||||||
return `「${issue.text}…」這句 ${issue.chars} 字,超過 ${MAX_SENTENCE_CHARS} 字——斷成兩句,或把修飾砍掉`;
|
return `「${issue.text}…」這句 ${issue.chars} 字,太長了(上限 ${MAX_SENTENCE_CHARS})——斷成兩句,或把修飾拿掉`;
|
||||||
case "explain":
|
case "explain":
|
||||||
return `「${issue.opener}」是在解釋自己剛講的話——刪掉,講完就算了`;
|
return `「${issue.opener}」是在回頭解釋自己剛講的話——拿掉,講完就停`;
|
||||||
case "hedge":
|
case "hedge":
|
||||||
return `「${issue.words.join("」「")}」一句疊了 ${issue.words.length} 層避險——確定的事零層,真的不確定留一層`;
|
return `一句話裡疊了 ${issue.words.length} 層「${issue.words.join("」「")}」——確定的事就直說,真的不確定留一個就好`;
|
||||||
case "cn":
|
case "cn":
|
||||||
return `「${issue.word}」是中國用語 → ${issue.suggest}`;
|
return `「${issue.word}」是中國用語,台灣這邊說「${issue.suggest}」`;
|
||||||
case "halfwidth":
|
case "halfwidth":
|
||||||
return `「${issue.text}」用了半形標點——中文句子一律全形`;
|
return `「${issue.text}」是半形標點——中文句子用全形`;
|
||||||
case "emoji":
|
case "emoji":
|
||||||
return `${issue.count} 個 emoji——講話最多一個`;
|
return `${issue.count} 個 emoji,太多了——講話留一個就夠`;
|
||||||
case "dash":
|
case "dash":
|
||||||
return `${issue.count} 個破折號——一輪最多一個,其餘改逗號或句號`;
|
return `${issue.count} 個破折號——一輪留一個,其他改成逗號或句號`;
|
||||||
case "markdown":
|
case "markdown":
|
||||||
return "粗體、清單符號或標題——講話沒有排版,收掉";
|
return "有粗體、清單或標題——講話的人不會排版,收掉";
|
||||||
case "parallel":
|
case "parallel":
|
||||||
return `「不是 A 而是 B」出現 ${issue.count} 次——一輪最多一次,其餘改直述`;
|
return `「不是 A 而是 B」講了 ${issue.count} 次——一輪一次就夠,其他改成直說`;
|
||||||
case "pastclaim":
|
case "pastclaim":
|
||||||
return `講到「${issue.word}」:先 recall 確認記憶裡真的有這件事,沒有紀錄就是編造自己的過去`;
|
return `你講到「${issue.word}」——先 recall 一下,記憶裡真的有這件事嗎?沒有的話那是在編自己的過去`;
|
||||||
default:
|
default:
|
||||||
return `「${issue.word}」是${issue.label}——${issue.fix}`;
|
return `「${issue.word}」——${issue.label}。${issue.fix}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2819,21 +2852,20 @@ export function turnContext(slug, sessionId, prompt = "") {
|
|||||||
`人格:\`${slug}\` ${identityBrief(slug)}`,
|
`人格:\`${slug}\` ${identityBrief(slug)}`,
|
||||||
`人格倉庫:${personaDir(slug)}(唯一可讀寫的人格資料範圍)`,
|
`人格倉庫:${personaDir(slug)}(唯一可讀寫的人格資料範圍)`,
|
||||||
emotionBrief(slug, state),
|
emotionBrief(slug, state),
|
||||||
`說話規則:回使用者 ${MAX_SENTENCES} 句以內(劇場模式每人每輪也一樣);` +
|
speakDirective(slug),
|
||||||
"推導、比對、盤算一律走心裡話(`persona.mjs think`),不要說給使用者聽——" +
|
"話短一點。一句講不完就斷開,不要用逗號一直串下去。用你平常會說的字," +
|
||||||
"要讓他知道你在想,就只報「心想 N 句」,不報內容。",
|
"看得見的東西先講——「你手上那杯冷了」比「我感覺到你的疲憊」好。" +
|
||||||
`講話像講話:**短句**(一句 ${MAX_SENTENCE_CHARS} 字內,長了就斷開)、**日常用詞**(不用書面語、` +
|
"講完就停,不要回頭解釋自己剛講的話,也不要幫自己收尾。",
|
||||||
"不用術語、不用成語堆疊)、**多講看得見的東西**(人、動作、東西、當下的場面)而不是概念與感想;" +
|
"有些句子一出口就不像人了:「這個我懂」「我完全理解」「好問題」" +
|
||||||
"**講完就算了**——不要解釋自己剛講的話(沒有「我的意思是」「換句話說」「也就是說」)," +
|
"(先發一句免費的認可,對方要的是回答)、「希望這對你有幫助」「接下來我會⋯」、" +
|
||||||
"不要補註解、不要收尾總結。這幾條 `room post` 與 `said check` 會實際擋下。",
|
"「說到底」「本質上」(假裝在講穿本質,其實只是重講一次)、開場的「老實說」、" +
|
||||||
"不要說 AI 才會說的話:罐頭同理心(「這個我懂」「我完全理解」)、頒獎開場(「好問題」)、" +
|
"收尾的「總的來說」、該表態時的「各有優缺點」、沒出處的「研究顯示」、" +
|
||||||
"交差句(「希望這對你有幫助」)、預告(「接下來我會」)、說教腔(「說到底」「本質上」)、" +
|
"用旁白演情緒的「我愣了一下」。這些不要說。",
|
||||||
"假坦白開場(「老實說」)、罐頭收尾(「總的來說」)、立場真空(「各有優缺點」「因人而異」)、" +
|
"**講自己的過去,要真的有那件事**。「我以前⋯」「我原本以為⋯」先 `recall` 查得到才講;" +
|
||||||
"無來源的權威(「研究顯示」)、用旁白演情緒(「我愣了一下」)、一句疊兩層避險、" +
|
"查不到就別講——編一段轉折比講一句空話糟糕得多。情緒也一樣," +
|
||||||
"中國用語與半形標點、粗體與清單符號。這些 `room post` 也會擋。",
|
"照現在的情緒值走,不要為了顯得有人味硬加。",
|
||||||
"**講自己的過去要有出處**:「我以前⋯」「我原本以為⋯」只能講記憶裡真的有的事(先 `recall` 查)。" +
|
`(機械會擋的:一句 ${MAX_SENTENCE_CHARS} 字、上面那些詞、中國用語、半形標點、` +
|
||||||
"沒有紀錄就不要講——編一段轉折比講一句空話糟糕得多。同理,情緒要來自現在的情緒值," +
|
"粗體與清單符號、兩小時內講過的話——`room post` 與 `said check` 直接拒收。)",
|
||||||
"不是為了有人味而加上去的。",
|
|
||||||
];
|
];
|
||||||
// 情緒先行:先讀對方那句話帶了什麼,再談我自己的狀態。
|
// 情緒先行:先讀對方那句話帶了什麼,再談我自己的狀態。
|
||||||
const read = readUserEmotion(prompt);
|
const read = readUserEmotion(prompt);
|
||||||
|
|||||||
+20
-15
@@ -802,11 +802,11 @@ commands.said = ({ flags, positional }) => {
|
|||||||
const lines = [];
|
const lines = [];
|
||||||
if (repeat) {
|
if (repeat) {
|
||||||
lines.push(
|
lines.push(
|
||||||
`⚠ 這句跟 ${repeat.minutes_ago} 分鐘前說過的話相似 ${repeat.similarity}:「${repeat.text.slice(0, 60)}」`,
|
`⚠ 這句 ${repeat.minutes_ago} 分鐘前講過了(像了 ${repeat.similarity}):「${repeat.text.slice(0, 60)}」`,
|
||||||
" → 換個角度、補新資訊,或直接推進話題;不要再說一次。",
|
" → 換個角度、補點新的,或直接推進話題。",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (tooLong) lines.push(`⚠ 這段有 ${sentences} 句,超過 ${pl.MAX_SENTENCES} 句上限 → 砍到重點。`);
|
if (tooLong) lines.push(`⚠ 講太多了,${sentences} 句(上限 ${pl.MAX_SENTENCES})→ 挑最想說的那一兩句。`);
|
||||||
const lint = pl.speechLint(text);
|
const lint = pl.speechLint(text);
|
||||||
const blockers = lint.filter((i) => i.level !== "hint");
|
const blockers = lint.filter((i) => i.level !== "hint");
|
||||||
for (const issue of blockers) lines.push(`⚠ ${pl.speechLintMessage(issue)}。`);
|
for (const issue of blockers) lines.push(`⚠ ${pl.speechLintMessage(issue)}。`);
|
||||||
@@ -814,7 +814,7 @@ commands.said = ({ flags, positional }) => {
|
|||||||
for (const issue of lint.filter((i) => i.level === "hint")) lines.push(`· ${pl.speechLintMessage(issue)}。`);
|
for (const issue of lint.filter((i) => i.level === "hint")) lines.push(`· ${pl.speechLintMessage(issue)}。`);
|
||||||
const clean = !repeat && !tooLong && !blockers.length;
|
const clean = !repeat && !tooLong && !blockers.length;
|
||||||
if (clean) {
|
if (clean) {
|
||||||
lines.push(`✔ 沒說過,${sentences} 句,可以說。`);
|
lines.push(`✔ 沒講過,${sentences} 句,可以說。`);
|
||||||
if (flags.record) pl.recordSaid(slug, text, { kind: "reply" });
|
if (flags.record) pl.recordSaid(slug, text, { kind: "reply" });
|
||||||
}
|
}
|
||||||
emit({ persona: slug, repeat, sentences, lint, ok: clean }, flags.json, lines);
|
emit({ persona: slug, repeat, sentences, lint, ok: clean }, flags.json, lines);
|
||||||
@@ -932,8 +932,8 @@ commands.emotion = ({ flags }) => {
|
|||||||
emit({ persona: slug, read }, flags.json, [
|
emit({ persona: slug, read }, flags.json, [
|
||||||
`讀「${readText.slice(0, 40)}」:`,
|
`讀「${readText.slice(0, 40)}」:`,
|
||||||
read.signals.length
|
read.signals.length
|
||||||
? ` 訊號:${read.signals.map((s) => `${s.zh} ${s.score}`).join("、")}|強度 ${read.intensity}|${read.confident ? "夠強,值得參考" : "偏弱,別當判定"}`
|
? ` 讀到:${read.signals.map((s) => `${s.zh} ${s.score}`).join("、")}|強度 ${read.intensity}|${read.confident ? "夠明顯,可以參考" : "很弱,別當真"}`
|
||||||
: " 沒有讀到明確的情緒訊號。",
|
: " 這句沒讀出什麼情緒。",
|
||||||
...(read.signals.length ? [` 怎麼接:${pl.RESPONSE_STANCE[read.signals[0].key]}`] : []),
|
...(read.signals.length ? [` 怎麼接:${pl.RESPONSE_STANCE[read.signals[0].key]}`] : []),
|
||||||
]);
|
]);
|
||||||
return;
|
return;
|
||||||
@@ -946,8 +946,8 @@ commands.emotion = ({ flags }) => {
|
|||||||
` 往舒服的方向 ${audit.positive}|往難受的方向 ${audit.negative}` +
|
` 往舒服的方向 ${audit.positive}|往難受的方向 ${audit.negative}` +
|
||||||
(audit.positive_ratio === null ? "" : ` → 正向佔 ${audit.positive_ratio}%`),
|
(audit.positive_ratio === null ? "" : ` → 正向佔 ${audit.positive_ratio}%`),
|
||||||
audit.positive_ratio !== null && audit.positive_ratio >= 90
|
audit.positive_ratio !== null && audit.positive_ratio >= 90
|
||||||
? " ⚠ 幾乎只給自己加分——delta 是你自己挑的,這種偏差要自己看得見。"
|
? " ⚠ 幾乎都在給自己加分。delta 是你自己挑的,這種偏差沒人會替你抓。"
|
||||||
: " 比例還算兩邊都有。",
|
: " 兩邊都有,還算誠實。",
|
||||||
trend ? ` 對方的走向:${trend.zh} ${trend.direction}(近 ${trend.rounds} 輪)` : " 還沒有足夠的輪次算走向。",
|
trend ? ` 對方的走向:${trend.zh} ${trend.direction}(近 ${trend.rounds} 輪)` : " 還沒有足夠的輪次算走向。",
|
||||||
]);
|
]);
|
||||||
return;
|
return;
|
||||||
@@ -1328,23 +1328,28 @@ commands.room = ({ flags, positional }) => {
|
|||||||
(speakerRole === "guest" ? null : pl.saidRepeat(speaker, text, opts));
|
(speakerRole === "guest" ? null : pl.saidRepeat(speaker, text, opts));
|
||||||
if (repeat) {
|
if (repeat) {
|
||||||
die(
|
die(
|
||||||
`\`${speaker}\` 在 ${repeat.minutes_ago} 分鐘前說過幾乎一樣的話(相似度 ${repeat.similarity}):` +
|
`這句 ${repeat.minutes_ago} 分鐘前就講過了(像了 ${repeat.similarity}):「${repeat.text.slice(0, 60)}」。` +
|
||||||
`「${repeat.text.slice(0, 60)}」。換個說法、補新東西或推進話題;` +
|
"換個角度、補點新的,或者直接把話題往前推。" +
|
||||||
"真的需要重複(例如被追問)才加 `--allow-repeat`。",
|
"他追問、你非得重講一次,才加 `--allow-repeat`。",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const sentences = pl.sentenceCount(text);
|
const sentences = pl.sentenceCount(text);
|
||||||
if (sentences > pl.MAX_SENTENCES && !flags.force) {
|
// 越害羞的人話越少:上限跟著羞恥度走,不是固定三句
|
||||||
|
const budget = pl.personaExists(speaker) ? pl.speechBudget(speaker) : { sentences: pl.MAX_SENTENCES, modesty: 50 };
|
||||||
|
if (sentences > budget.sentences && !flags.force) {
|
||||||
|
const why = budget.sentences < pl.MAX_SENTENCES
|
||||||
|
? `(羞恥度 ${budget.modesty},這個人格話比別人少)`
|
||||||
|
: "";
|
||||||
die(
|
die(
|
||||||
`這句有 ${sentences} 句,超過 ${pl.MAX_SENTENCES} 句上限——聊天不是報告,砍到重點再發。` +
|
`講太多了,${sentences} 句。這裡最多 ${budget.sentences} 句${why}——` +
|
||||||
"(真的需要長段落才加 `--force`。)",
|
"挑最想說的那一兩句就好,剩下的寫進心裡話。(真的需要長台詞才加 `--force`。)",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// 短句、日常話、講完不解釋、不說 AI 才會說的話——講話的樣子也擋在 CLI 裡,不靠自律。
|
// 短句、日常話、講完不解釋、不說 AI 才會說的話——講話的樣子也擋在 CLI 裡,不靠自律。
|
||||||
const lint = pl.speechBlockers(text);
|
const lint = pl.speechBlockers(text);
|
||||||
if (lint.length && !flags.force) {
|
if (lint.length && !flags.force) {
|
||||||
die(`講話規則:${lint.map(pl.speechLintMessage).join(";")}。(真的需要才加 \`--force\`。)`);
|
die(`${lint.map(pl.speechLintMessage).join(";")}。(真的需要才加 \`--force\`。)`);
|
||||||
}
|
}
|
||||||
let emotion = str(flags.emotion);
|
let emotion = str(flags.emotion);
|
||||||
if (!emotion && pl.personaExists(speaker)) {
|
if (!emotion && pl.personaExists(speaker)) {
|
||||||
|
|||||||
+33
-8
@@ -376,7 +376,7 @@ check("said check 抓到超過三句", (() => {
|
|||||||
})(), longSay.stdout.slice(0, 160));
|
})(), longSay.stdout.slice(0, 160));
|
||||||
const ctxSpeak = pl.turnContext("alpha", S_SPEAK, "報告");
|
const ctxSpeak = pl.turnContext("alpha", S_SPEAK, "報告");
|
||||||
check("turnContext 注入說話規則 / 心裡話 / 最近說過",
|
check("turnContext 注入說話規則 / 心裡話 / 最近說過",
|
||||||
ctxSpeak.includes("說話規則") && ctxSpeak.includes("💭") && ctxSpeak.includes("最近說過的話"),
|
ctxSpeak.includes("最多講") && ctxSpeak.includes("心裡話") && ctxSpeak.includes("💭") && ctxSpeak.includes("最近說過的話"),
|
||||||
ctxSpeak.slice(0, 400));
|
ctxSpeak.slice(0, 400));
|
||||||
cli(["invite", "--session", S_SPEAK, "--guest", "beta", "--topic", "節制測試"]);
|
cli(["invite", "--session", S_SPEAK, "--guest", "beta", "--topic", "節制測試"]);
|
||||||
const room2 = pl.loadSession(S_SPEAK).guests?.beta?.room;
|
const room2 = pl.loadSession(S_SPEAK).guests?.beta?.room;
|
||||||
@@ -388,14 +388,15 @@ cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha", "--
|
|||||||
const repeatPost = cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
|
const repeatPost = cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
|
||||||
"--text", "這次的鐘你修得比上次穩。"], { expectOk: false });
|
"--text", "這次的鐘你修得比上次穩。"], { expectOk: false });
|
||||||
check("room post 擋下近似重複的台詞",
|
check("room post 擋下近似重複的台詞",
|
||||||
repeatPost.status !== 0 && repeatPost.stderr.includes("說過"), repeatPost.stderr.slice(0, 160));
|
repeatPost.status !== 0 && repeatPost.stderr.includes("講過"), repeatPost.stderr.slice(0, 160));
|
||||||
check("--allow-repeat 可以放行",
|
check("--allow-repeat 可以放行",
|
||||||
cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
|
cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
|
||||||
"--text", "這次的鐘你修得比上次穩。", "--allow-repeat"]).status === 0);
|
"--text", "這次的鐘你修得比上次穩。", "--allow-repeat"]).status === 0);
|
||||||
const longPost = cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
|
const longPost = cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
|
||||||
"--text", "第一句在這。第二句在這。第三句在這。第四句在這。"], { expectOk: false });
|
"--text", "第一句在這。第二句在這。第三句在這。第四句在這。"], { expectOk: false });
|
||||||
check("room post 擋下超過三句的發言",
|
check("room post 擋下超過三句的發言",
|
||||||
longPost.status !== 0 && longPost.stderr.includes("超過"), longPost.stderr.slice(0, 160));
|
longPost.status !== 0 && longPost.stderr.includes("最多") && longPost.stderr.includes("句"),
|
||||||
|
longPost.stderr.slice(0, 160));
|
||||||
// 講話的樣子:短句、日常話、講完不解釋。
|
// 講話的樣子:短句、日常話、講完不解釋。
|
||||||
const longSentence = cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
|
const longSentence = cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
|
||||||
"--text", "這件事情其實從很久以前就一直放在我心裡面沒有講出來過,因為我一直覺得時機不太對而且你最近也真的很忙。"],
|
"--text", "這件事情其實從很久以前就一直放在我心裡面沒有講出來過,因為我一直覺得時機不太對而且你最近也真的很忙。"],
|
||||||
@@ -405,7 +406,7 @@ check("room post 擋下太長的句子",
|
|||||||
const explainPost = cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
|
const explainPost = cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
|
||||||
"--text", "我的意思是,那台鐘該修了。"], { expectOk: false });
|
"--text", "我的意思是,那台鐘該修了。"], { expectOk: false });
|
||||||
check("room post 擋下解釋自己的話(我的意思是…)",
|
check("room post 擋下解釋自己的話(我的意思是…)",
|
||||||
explainPost.status !== 0 && explainPost.stderr.includes("講完就算了"), explainPost.stderr.slice(0, 200));
|
explainPost.status !== 0 && explainPost.stderr.includes("講完就停"), explainPost.stderr.slice(0, 200));
|
||||||
check("--force 仍可放行(真的需要長台詞時)",
|
check("--force 仍可放行(真的需要長台詞時)",
|
||||||
cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
|
cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
|
||||||
"--text", "我的意思是,那台鐘該修了。", "--force"]).status === 0);
|
"--text", "我的意思是,那台鐘該修了。", "--force"]).status === 0);
|
||||||
@@ -471,8 +472,8 @@ check("提醒的句子 room post 照樣發得出去",
|
|||||||
cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
|
cli(["room", "post", "--session", S_SPEAK, "--room", room2, "--as", "alpha",
|
||||||
"--text", "我以前也修過一台一樣的。"]).status === 0);
|
"--text", "我以前也修過一台一樣的。"]).status === 0);
|
||||||
const voiceCtx = pl.turnContext("alpha", S_SPEAK, "你在幹嘛");
|
const voiceCtx = pl.turnContext("alpha", S_SPEAK, "你在幹嘛");
|
||||||
check("說話規則有注入黑名單與「過去要有出處」",
|
check("說話規則有注入黑名單與「講過去要有出處」",
|
||||||
voiceCtx.includes("罐頭同理心") && voiceCtx.includes("要有出處"));
|
voiceCtx.includes("這個我懂") && voiceCtx.includes("真的有那件事"), voiceCtx.slice(0, 200));
|
||||||
|
|
||||||
// --------------------------------------------------------------------------- //
|
// --------------------------------------------------------------------------- //
|
||||||
// 情緒處理:讀對方那句話(輸入端)、走向、飽和與單輪預算
|
// 情緒處理:讀對方那句話(輸入端)、走向、飽和與單輪預算
|
||||||
@@ -508,6 +509,30 @@ check("標點只放大既有訊號,不憑空長出新情緒",
|
|||||||
check("每種情緒都有對應的「怎麼接」",
|
check("每種情緒都有對應的「怎麼接」",
|
||||||
pl.EMOTION_KEYS.every((k) => typeof pl.RESPONSE_STANCE[k] === "string" && pl.RESPONSE_STANCE[k].length > 4));
|
pl.EMOTION_KEYS.every((k) => typeof pl.RESPONSE_STANCE[k] === "string" && pl.RESPONSE_STANCE[k].length > 4));
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------- //
|
||||||
|
// 越害羞的人話越少、心裡話越多
|
||||||
|
console.log("\n羞恥度 → 話量與心裡話的比例");
|
||||||
|
{
|
||||||
|
const mk = (slug, soul) => {
|
||||||
|
fs.mkdirSync(path.join(H, slug), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(H, slug, "IDENTITY.md"), "- Name: X\n- Creature: 人類女性\n");
|
||||||
|
fs.writeFileSync(path.join(H, slug, "SOUL.md"), soul);
|
||||||
|
};
|
||||||
|
mk("shy", "很怕生,被稱讚會臉紅,容易不好意思。");
|
||||||
|
mk("bold", "我行我素,不在意別人眼光,臉皮厚。");
|
||||||
|
const shy = pl.speechBudget("shy");
|
||||||
|
const bold = pl.speechBudget("bold");
|
||||||
|
check("越害羞話越少(句數上限跟著羞恥度走)",
|
||||||
|
shy.sentences < bold.sentences && bold.sentences === pl.MAX_SENTENCES,
|
||||||
|
`shy=${shy.sentences} bold=${bold.sentences}`);
|
||||||
|
check("越害羞心裡話越多(下限跟著羞恥度走)",
|
||||||
|
shy.thinkMin > bold.thinkMin, `shy=${shy.thinkMin} bold=${bold.thinkMin}`);
|
||||||
|
check("高羞恥的人被提醒「台詞是心裡話的迴避」",
|
||||||
|
shy.note.includes("迴避") && pl.speakDirective("shy").includes("先寫再開口"), shy.note);
|
||||||
|
check("不害羞的人不用先在心裡繞一圈",
|
||||||
|
bold.thinkMin === 0 && bold.note.includes("想到什麼就講"), bold.note);
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------- //
|
// --------------------------------------------------------------------------- //
|
||||||
// 性別 → 羞恥敏感度:性別只給預設值,描述永遠蓋過它
|
// 性別 → 羞恥敏感度:性別只給預設值,描述永遠蓋過它
|
||||||
console.log("\n性別與羞恥敏感度(描述優先,性別只是預設)");
|
console.log("\n性別與羞恥敏感度(描述優先,性別只是預設)");
|
||||||
@@ -611,9 +636,9 @@ check("情緒的破口有注入,且說明是演出來不是講出來", (() =>
|
|||||||
})(), pl.turnContext("alpha", S_SPEAK, "那件事做完了嗎").slice(0, 900));
|
})(), pl.turnContext("alpha", S_SPEAK, "那件事做完了嗎").slice(0, 900));
|
||||||
check("十二情緒每一種都有破口可演",
|
check("十二情緒每一種都有破口可演",
|
||||||
pl.EMOTION_KEYS.every((k) => (pl.EMOTION_TELLS[k] || []).length >= 3));
|
pl.EMOTION_KEYS.every((k) => (pl.EMOTION_TELLS[k] || []).length >= 3));
|
||||||
check("說話規則有注入「短句/日常用詞/看得見的東西/不要解釋」", (() => {
|
check("說話規則有注入「話短、日常字、看得見的東西、不要回頭解釋」", (() => {
|
||||||
const ctxSay = pl.turnContext("alpha", S_SPEAK, "隨便講");
|
const ctxSay = pl.turnContext("alpha", S_SPEAK, "隨便講");
|
||||||
return ["短句", "日常用詞", "看得見的東西", "不要解釋自己剛講的話"].every((w) => ctxSay.includes(w));
|
return ["話短一點", "平常會說的字", "看得見的東西", "不要回頭解釋"].every((w) => ctxSay.includes(w));
|
||||||
})(), pl.turnContext("alpha", S_SPEAK, "隨便講").slice(0, 600));
|
})(), pl.turnContext("alpha", S_SPEAK, "隨便講").slice(0, 600));
|
||||||
check("說出口的話有進 said.jsonl",
|
check("說出口的話有進 said.jsonl",
|
||||||
pl.readJsonl(pl.saidPath("alpha")).some((r) => String(r.text).includes("鐘修得比上次穩")));
|
pl.readJsonl(pl.saidPath("alpha")).some((r) => String(r.text).includes("鐘修得比上次穩")));
|
||||||
|
|||||||
@@ -44,25 +44,33 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/persona.mjs" show --persona <slug> --session
|
|||||||
|
|
||||||
## 2. 說話的四條鐵則(先讀這段,比什麼都重要)
|
## 2. 說話的四條鐵則(先讀這段,比什麼都重要)
|
||||||
|
|
||||||
**① 一到三句。** 正常人聊天不會一次講五段。回使用者的話 **1–3 句**就好;
|
**① 話少一點。** 沒有人聊天會一次講五段。回他 **1–3 句**就夠;
|
||||||
真的需要長內容(清單、程式碼、他明確要求的說明)才例外,而且要先問或直接給重點。
|
真的要給長東西(清單、程式碼、他點名要的說明)才例外,而且先問一句或直接給重點。
|
||||||
|
|
||||||
**② 推導不說出口。** 你怎麼從他那句話推到結論、比對到哪則記憶、猜他心情如何——
|
> 這個上限**跟著羞恥度走**:越害羞的人話越少(`speechBudget()`)。
|
||||||
這些是**心裡話**,寫進 `think`,不要打在畫面上。使用者要看的是「你這個人怎麼回應」,
|
> 羞恥度 ≥ 75 的人格上限是 2 句,`room post` 會照這個擋。
|
||||||
不是「你怎麼算出來的」。要讓他知道你在想,只報**狀態**(`💭 心想 3 句`),不報內容。
|
|
||||||
|
|
||||||
**③ 說過的別再說。** 同一件事換句話講一遍,是 AI 才會做的事。
|
**② 想的事不要說出來。** 你怎麼從他那句話推到結論、對到哪則記憶、猜他心情如何——
|
||||||
|
那些是**心裡話**,寫進 `think`,不要打在畫面上。他要看的是你這個人怎麼回應,
|
||||||
|
不是你怎麼算出來的。想讓他知道你在想,只報**狀態**(`💭 心想 3 句`),內容不報。
|
||||||
|
|
||||||
|
> **越害羞的人,心裡話比說出口的話重要。** 羞恥度 ≥ 75 的時候,
|
||||||
|
> 這一輪至少要先寫 2 句心裡話再開口,而且**說出口的那句常常正好在迴避心裡那句**——
|
||||||
|
> 心裡想「我一直在等你問」,出口就不能是「我有在等你問」,那等於把心裡話搬到台面上。
|
||||||
|
> 落差本身才是那個角色。羞恥度低的人不需要這個緩衝,想到什麼就講。
|
||||||
|
|
||||||
|
**③ 說過的別再說一次。** 同一件事換句話再講一遍,只有 AI 會這樣。
|
||||||
`<persona-context>` 每輪都會列「最近說過的話」——那些內容 **2 小時內不要重講**:
|
`<persona-context>` 每輪都會列「最近說過的話」——那些內容 **2 小時內不要重講**:
|
||||||
要嘛換個角度、補新資訊,要嘛推進話題,要嘛就閉嘴聽他說。
|
要嘛換個角度、補新資訊,要嘛推進話題,要嘛就閉嘴聽他說。
|
||||||
|
|
||||||
**④ 講話像講話。** 五件小事,決定聽起來是人還是模型:
|
**④ 講話就像在講話。** 五件小事,決定你聽起來是人還是模型:
|
||||||
|
|
||||||
- **短句**。一句 45 字以內,長了就斷開。逗號串成一大串是寫作,不是講話。
|
- **話短一點**。一句 45 字以內,講不完就斷開。逗號串成一長條那是在寫東西,不是在講話。
|
||||||
- **日常用詞**。用你平常會說的字;不用書面語、不堆術語與成語。
|
- **用平常會說的字**。不要書面語,不要堆術語和成語。
|
||||||
- **講看得見的東西**。人、動作、東西、當下的場面——具體的先講。
|
- **先講看得見的東西**。人、動作、物件、當下的場面,具體的先說。
|
||||||
「你手上那杯已經冷了」勝過「我感覺到你的疲憊」。
|
「你手上那杯已經冷了」勝過「我感覺到你的疲憊」。
|
||||||
- **講完就算了**。不要解釋自己剛講的話:沒有「我的意思是」「換句話說」「也就是說」,
|
- **講完就停**。不要回頭解釋自己剛講的話(「我的意思是」「換句話說」「也就是說」),
|
||||||
也不要補註解或收尾總結。對方沒聽懂會問,他問了再說。
|
也不要補註解、不要幫自己收尾。他沒聽懂會問,問了再說。
|
||||||
- **讓情緒改變句子的形狀**。焦慮 → 句子斷在一半、疊字(「我、我知道」);羞愧 →
|
- **讓情緒改變句子的形狀**。焦慮 → 句子斷在一半、疊字(「我、我知道」);羞愧 →
|
||||||
鬧彆扭,先否認再小聲承認;憤怒 → 短句、稱呼退回全名;悲傷 → 只回一個詞。
|
鬧彆扭,先否認再小聲承認;憤怒 → 短句、稱呼退回全名;悲傷 → 只回一個詞。
|
||||||
對照表在 `reference/emotions.md`(`EMOTION_TELLS`),`<persona-context>` 每輪會直接
|
對照表在 `reference/emotions.md`(`EMOTION_TELLS`),`<persona-context>` 每輪會直接
|
||||||
|
|||||||
Reference in New Issue
Block a user