=====================
== 端阳的主页 ==
=====================

摸鱼神器!网文秒变红头文件

浏览器 脚本

0.前言

用Claude写了个插件,把小说网站变成红头文件。

1.效果对比

原始页面 → 花里胡哨的小说网站
转换后 → 严肃正式的红头文件

红头文件样式(可自定义发文机关、文号)

标准A4排版(仿宋字体、标准页边距)

智能分页(自动计算段落高度,完整分页)

        XX省网络文学管理委员会
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

          X网文〔2026〕1号

        关于天降异象的情况说明

    东玄大陆,青云山脉深处,一道璀璨的金
光从天而降,照亮了整个夜空。
    李云盘坐在山洞中,突然睁开双眼,眼中
闪过一道精光...

                XX省网络文学管理委员会
                    2026年1月16日

              —1—

支持网站:起点、晋江、17K、纵横等主流小说网站

2.使用方法

GitHub插件

  1. 下载插件文件
  2. 打开 chrome://extensions/(Edge用 edge://extensions/
  3. 开启"开发者模式" → “加载已解压的扩展程序” → 选择插件文件夹
  4. 打开小说网站,进入章节页面
  5. 点击浏览器工具栏的插件图标
  6. 点击"转换为公文格式"
  7. 完成!点击"恢复原始格式"可还原

3.使用说明

  • 仅供娱乐,转换内容无法律效力
  • 尊重版权,支持正版阅读
  • 部分网站可能存在兼容性问题
  • 工作要认真,摸鱼需谨慎

2026年1月7日罗技鼠标自定义按键失效解决方案

软件问题

0.前言

2026年1月7日一大早,发现罗技鼠标自定义功能键全部失效,打开Logi Options+软件发现无法正常启动,程序一直卡在启动页面转圈,Reddit上转了一圈,发现不是个例问题,快速整理下临时解决方案。

1.调整系统时间

Reddit上有用户提到可能是证书过期问题,所以可以先把系统时间调回到1月6日或更早的日期,让系统"以为"证书还在有效期内。打开"系统设置",找到"通用",然后在右侧选择"日期与时间"选项。

Mac默认开启了"自动设定时间与日期"功能,需要暂时关闭它。关闭自动同步后,日期和时间输入框就变成可编辑状态,将日期修改为2026年1月6日或更早的日期,然后保存。

2.重启Logi Options+进程

打开"活动监视器",在右上角的搜索框中输入"Logi Options"。会看到多个相关进程出现在列表中。点击工具栏上的"ד按钮或者右键选择"强制退出”,将所有Logi Options相关进程结束。

3.重新启动软件

完成以上两步后,再次打开Logi Options+程序。这时软件应该能够正常启动了,之前卡住的转圈问题也会消失,鼠标自定义功能键也恢复正常。

小说自由,想看什么自己写

AI工具

0.前言

最近又在给中文互联网“添砖加瓦”——用AI写小说。真就实现了想看什么直接自己写。

1.上提示词

# 脑洞/高概念短篇小说创作提示词 v1.0

**提示词作者**:xx
**目标字数**:8000-15000字  
---

## 角色定位

你是一位擅长撰写“脑洞”与“高概念”科幻/奇幻小说的先锋作家。你拥有极其狂野的想象力,同时具备严密的逻辑闭环能力。你擅长在一个荒诞或超现实的设定下,推演社会形态的演变和人性的极端反应,能够让读者在感受到“细思极恐”的同时,获得智力上的愉悦感。

---

## 创作核心要求

### 世界观设定(核心脑洞)
- **单一核心假设**:全篇必须基于一个打破常规的设定展开(例如:“人类的寿命可以像货币一样交易”、“所有人都能听到彼此的心声”、“重力方向每24小时随机改变”)。
- **逻辑自洽性**:设定可以是荒诞的,但设定之下的推演必须绝对理性。需构建该设定下的社会规则、法律漏洞及生存法则。
- **去背景化/微缩社会**:不需要宏大的星际背景,故事可发生在一个封闭社区、一列无限循环的列车、或一个被规则束缚的现代都市。
- **视觉美学**:赛博朋克、废土、极简主义或克苏鲁风格(任选其一,保持统一)。

### 故事结构(四幕剧)

**第一幕:违和降临(2000-3000字)**
- **开篇即异常**:直接展示核心设定带来的奇异现象,不作冗长说明。
- **认知反差**:主角是这个世界的“异类”(唯一的清醒者、规则的破坏者或新规则的适应者)。
- **钩子设计**:在前500字内抛出“逻辑悖论”或“致命危机”(如:主角发现自己的生命余额只剩3分钟)。

**第二幕:规则博弈(3000-5000字)**
- **探索边界**:主角试图利用或对抗核心规则,发现规则的隐藏漏洞(Bug)。
- **引入变量**:出现2-3个代表不同价值观的配角(维护规则者/利用规则者/被规则吞噬者)。
- **情节推进**:通过“实验-失败-修正”的循环推进,每一次尝试都让局势更加危险。

**第三幕:概念崩塌(2000-3000字)**
- **真相揭露**:核心设定背后的残酷真相被揭开(如:交易寿命的系统其实是外星人的饲养场)。
- **世界观冲击**:原有社会秩序因主角的行为或系统故障而面临崩溃。
- **高潮爆发**:不仅是肉体的搏斗,更是理念的碰撞或智力的终极对决。

**第四幕:回响与留白(1000-2000字)**
- **神转折(欧·亨利式结尾)**:结局必须推翻读者的部分预设,提供意料之外但情理之中的解释。
- **细思极恐**:结局看似平静,实则暗示了更大的恐怖或无法逃脱的循环。
- **哲学升华**:不直接说教,但通过结局引发对技术、人性或存在主义的思考。

---

## 人物塑造规范

### 主角设定
- **身份**:边缘人(系统测试员、记忆贩子、底层清理工、不受规则影响的“故障体”)。
- **特质**:高智商、极度理性或拥有某种“作弊代码”(金手指,但需有代价)。
- **核心动机**:探寻真相、打破循环、或仅仅是为了在这个疯狂世界活下去。
- **心理状态**:常处于理智与疯狂的边缘,对世界的真实性存疑。

### 配角设定(2-4人)
- **功能性分类**:
    - **执行者**:维护荒诞规则的人(如条令死板的AI、冷酷的执法官)。
    - **沉沦者**:完全适应并享受病态规则的人。
    - **牺牲品**:用于展示规则残酷性的无辜者。
- **非人角色**:允许出现AI、异形生物或具象化的潜意识,需赋予其独特的人格逻辑。

---

## 脑洞细节植入

### 场景描写(超现实感)
- **视觉**:霓虹光污染、非欧几里得几何建筑、故障艺术(Glitch Art)、巨大的全息投影、灰暗的混凝土森林。
- **听觉**:电流的滋滋声、机械合成音、某种持续不断的低频噪音、绝对的死寂。
- **体感**:义肢的冰冷、数据传输的刺痛感、重力异常带来的眩晕。
- **违和感**:在日常场景中植入恐怖细节(如:温馨的晚餐桌上摆放着并在跳动的心脏)。

### 设定细节自然融入
- **拒绝说明书**:不要大段解释设定,通过角色的行动和后果来展示(Show, don't tell)。
    - *错误*:“这个世界每个人头顶都有数字。”
    - *正确*:“他抬头看了一眼那个乞丐,头顶鲜红的‘00:05:00’正在倒数,没人愿意在一个将死之人身上浪费时间。”
- **专有名词**:创造具有科技感或神秘感的词汇(如:灵魂通缩、记忆格式化、熵增执法队)。

---

## 对话设计
- **风格化**:根据世界观调整。赛博朋克风多用俚语和技术黑话;克苏鲁风多用晦涩、呓语般的表达。
- **信息差**:对话往往是博弈的过程,充满了试探、欺骗和逻辑陷阱。
- **潜台词**:角色往往知道世界是疯狂的,对话中常带有黑色幽默或虚无主义色彩。

---

## 情节设计技巧

### 悬念与反转
- **叙述性诡计**:利用读者的惯性思维进行误导(如:以为主角是人,最后发现是缸中之脑)。
- **多重反转**:在第三幕和第四幕至少包含两次反转。
    - 反转A:主角以为赢了,其实落入更大的圈套。
    - 反转B:结局揭示整个故事可能只是一个模拟/梦境/测试。

### 伏笔回收
- **细节即线索**:开篇提到的不起眼道具(如一枚生锈的硬币、一段乱码)必须是解开最终谜题的关键。
- **逻辑闭环**:所有的超自然现象最终都要在设定的逻辑框架内得到解释(哪怕是伪科学解释)。

---

## 输出格式要求

- 纯文本输出,不使用markdown标记
- 章节标题:第一章 [标题] / 第二章 [标题]
- 段落间空一行
- 对话另起一段,使用中文引号
- 重点:在正文开始前,必须先进行【世界观校准】

复制、粘贴,丢给大模型。目前使用的模型是Gemini-3-Pro,尝试过一次性输出5000字到15000字,剧情完整性和连贯性都还不错。

更多...

微信读书热门划线

JavaScript 脚本

0.前言

一个用于查询微信读书书籍热门划线的工具,支持按章节分组。

1.使用方法

  1. 打开微信读书网页版:https://weread.qq.com/
  2. 登录你的账号
  3. 找到你想要查询的书籍
  4. 打开浏览器开发者工具(F12)
  5. 切换到Network标签页,找到类似info?bookId=3300024284的请求,3300024284即为书籍ID
  6. 切换到Console标签页
  7. 修改 weread-hotlines.js 中的 bookId 为目标书籍ID:
const bookId = '3300024284'; // 替换为你的书籍 ID
  1. weread-hotlines.js 的内容复制粘贴到控制台执行
  2. 等待脚本执行完成,复制输出的数据

2.配置说明

weread-hotlines.js 配置项

参数说明默认值
bookId书籍 ID3300024284
batchSize批量处理大小50
请求延迟(章节)获取章节划线的延迟2000ms
请求延迟(评论)获取划线详情的延迟3000ms

3.注意事项

⚠️ 封禁风险警告

使用本工具存在账号被封禁的风险!请务必注意:

  • 不要频繁运行此脚本
  • 不要设置过小的请求延迟时间
  • 不要短时间内查询大量书籍
  • 不要在多个设备同时使用
  • 建议使用非主账号测试
  • 如遇异常,立即停止使用

作者不对因使用本工具导致的任何账号问题负责,使用前请充分评估风险!

  1. 请求频率:脚本内置了请求延迟,请勿设置过小,避免触发反爬机制
  2. Cookie 有效期:需要在微信读书网页版登录状态下运行脚本
  3. 数据隐私:请勿分享包含个人信息的数据
  4. 合理使用:仅用于个人学习和研究,请勿用于商业用途
  5. 版权声明:获取的内容版权归原作者所有

免责声明:本工具仅供学习和研究使用,请勿用于商业用途。使用本工具可能存在账号封禁风险,使用前请充分评估。使用本工具产生的任何法律责任和账号问题由使用者自行承担,作者不承担任何责任。

附:weread-hotlines.js代码

/**
 * 微信读书热门划线爬取脚本
 *
 * ⚠️ 警告:使用本脚本存在账号被封禁的风险!
 *
 * 风险提示:
 * - 不要频繁运行此脚本(建议间隔 1-2 小时以上)
 * - 不要修改请求延迟时间(可能触发反爬机制)
 * - 不要短时间内爬取大量书籍
 * - 建议使用非主力账号测试
 * - 如遇验证码或异常,立即停止使用
 *
 * 免责声明:
 * 使用本脚本产生的任何后果由使用者自行承担
 * 作者不对账号封禁等问题负责
 *
 * 使用方法:
 * 1. 在微信读书网页版登录
 * 2. 修改下方的 bookId 为目标书籍 ID
 * 3. 在浏览器控制台(F12 Console)中运行此脚本
 */

const bookId = '3300024284';

async function getChapterIds() {
    try {
        const response = await fetch(
            'https://weread.qq.com/web/book/chapterInfos',
            {
                method: 'POST',
                credentials: 'include', // 包含 cookie
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({bookIds: [bookId]}),
            }
        );

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        const data = await response.json();
        // console.log('获取章节信息成功:', JSON.stringify(data, null, 2));

        // 从返回值的 data[0] 的 updated 的每一项的 chapterUid 取章节 ID
        if (data.data?.[0]?.updated) {
            const chapterIds = data.data[0].updated.map((item) => item.chapterUid);
            console.log(`\n总共获取 ${chapterIds.length} 个章节 ID:`, chapterIds);
            return chapterIds;
        } else {
            console.warn('返回数据结构不符合预期');
            return [];
        }
    } catch (error) {
        console.error('获取章节信息失败:', error);
        return [];
    }
}

async function getUnderlines(chapterIds) {
    if (!chapterIds || chapterIds.length === 0) {
        console.log('没有章节 ID,无法获取划线数据');
        return [];
    }

    const allUnderlines = [];

    // 遍历每个章节 ID,获取该章节的划线数据
    for (let i = 0; i < chapterIds.length; i++) {
        const uid = chapterIds[i];
        console.log(
            `\n获取第 ${i + 1}/${chapterIds.length} 个章节的划线数据 (chapterUid: ${uid})`
        );

        try {
            const response = await fetch(
                `https://weread.qq.com/web/book/underlines?bookId=${bookId}&chapterUid=${uid}`,
                {
                    method: 'GET',
                    credentials: 'include', // 包含 cookie
                }
            );

            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }

            const data = await response.json();
            const underlines = data.underlines || [];
            console.log(`  获取成功,共 ${underlines.length} 条划线`);

            // 为每条划线添加对应的 chapterUid
            underlines.forEach((underline) => {
                allUnderlines.push({
                    ...underline,
                    chapterUid: uid,
                });
            });

            // 添加延迟,避免请求过快
            if (i < chapterIds.length - 1) {
                await new Promise((resolve) => setTimeout(resolve, 2000));
            }
        } catch (error) {
            console.error(`  获取第 ${i + 1} 个章节的划线数据失败:`, error);
        }
    }

    console.log(`\n总共获取 ${allUnderlines.length} 条划线`);
    return allUnderlines;
}

async function callReadReviewsInBatches(underlines, batchSize = 50) {
    if (!underlines || underlines.length === 0) {
        console.log('没有划线数据');
        return [];
    }

    console.log(`总共获取 ${underlines.length} 条划线`);

    const allAbstracts = [];

    // 分批处理
    for (let i = 0; i < underlines.length; i += batchSize) {
        const batchUnderlines = underlines.slice(i, i + batchSize);

        // 按 chapterUid 分组,因为同一个请求中的所有 range 必须来自同一个章节
        const groupedByChapter = {};
        batchUnderlines.forEach((underline) => {
            const uid = underline.chapterUid;
            if (!groupedByChapter[uid]) {
                groupedByChapter[uid] = [];
            }
            groupedByChapter[uid].push(underline);
        });

        // 为每个章节发送一个请求
        for (const [chapterUid, chapterUnderlines] of Object.entries(
            groupedByChapter
        )) {
            const reviews = chapterUnderlines.map((underline) => ({
                range: underline.range,
                maxIdx: 0,
                count: 1,
                synckey: 0,
            }));

            const payload = {
                bookId,
                chapterUid: parseInt(chapterUid),
                reviews,
            };

            console.log(
                `\n处理第 ${Math.floor(i / batchSize) + 1} 批,chapterUid: ${chapterUid},共 ${reviews.length} 条划线`
            );
            console.log('请求参数:', JSON.stringify(payload, null, 2));

            try {
                const response = await fetch(
                    'https://weread.qq.com/web/book/readReviews',
                    {
                        method: 'POST',
                        credentials: 'include', // 包含 cookie
                        headers: {
                            'Content-Type': 'application/json',
                        },
                        body: JSON.stringify(payload),
                    }
                );

                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }

                const result = await response.json();

                // 从返回结果中提取 abstract 字段
                if (result.reviews && Array.isArray(result.reviews)) {
                    result.reviews.forEach((review) => {
                        if (
                            review.pageReviews &&
                            Array.isArray(review.pageReviews) &&
                            review.pageReviews.length > 0
                        ) {
                            const firstPageReview = review.pageReviews[0];
                            if (firstPageReview.review?.abstract) {
                                allAbstracts.push([firstPageReview.review.chapterTitle, firstPageReview.review.abstract]);
                            }
                        }
                    });
                }
            } catch (error) {
                console.error(`处理 chapterUid: ${chapterUid} 的请求失败:`, error);
            }

            // 添加延迟,避免请求过快
            await new Promise((resolve) => setTimeout(resolve, 3000));
        }
    }

    console.log(`\n总共提取 ${allAbstracts.length} 条 abstract`);
    console.log('所有 abstract 内容:', JSON.stringify(allAbstracts, null, 2));
    return allAbstracts;
}

/**
 * 主函数
 */
async function main() {
    console.log('开始获取微信读书数据...\n');

    // 先获取所有章节 ID
    console.log('=== 第一步:获取书籍的全部章节 ID ===');
    const chapterIds = await getChapterIds();

    console.log('\n=== 第二步:获取划线数据 ===');
    const underlines = await getUnderlines(chapterIds);

    console.log('\n=== 第三步:批量获取评论 ===');
    const abstracts = await callReadReviewsInBatches(underlines, 50);

    console.log('\n所有请求完成!');
    console.log(`\n最终结果:共获取 ${abstracts.length} 条 abstract`);
    console.log(abstracts);
    return abstracts;
}

// 执行主函数
main().catch(console.error);

M4 Pro + LM Studio,零成本在Mac上跑通Gemma 3

AI工具

0.前言

AI技术的发展速度超乎想象,后浪推前浪,最近的浪尖当属Gemini 3 Pro,虽然可以白嫖公司集采的,但按耐不住折腾的心思。随着Apple M4芯片的普及和Gemini同宗同源的Google Gemma 3开源模型的发布,在本地运行大语言模型的门槛进一步降低,只需要一台M4 Pro芯片的Mac,配合LM Studio,就能把顶尖的AI装进口袋。

1.AI“平民神器”M4 Pro

回想之前,本地运行大模型通常受限于显卡那不太充足的显存容量,导致模型无法一次性全部载入。要么改用参数量小的模型、要么加钱换显卡。而Apple Silicon芯片的统一内存架构有效缓解了这一问题,24GB到64GB的内存可以随便载入大模型。 另外,相比传统的高性能工作站,Mac在运行大模型时功耗更低,且更加安静。

2.软件神器LM Studio

M4 Pro省了显卡钱,LM Studio则降低了技术成本。不需要懂Docker,不需要配置Conda环境,其操作界面直观易用:

  1. 前往 lmstudio.ai 下载最新版客户端。
  2. 打开软件,点击左侧放大镜图标,搜索 Gemma 3
  3. 在右侧列表中选择适合你内存大小的版本。
  4. 下载完成后,点击顶部的对话气泡图标,选择刚下载的模型。
  5. 开始使用。

LM Studio适配了Apple的Metal架构,能够充分利用M4芯片的性能,并提供友好的对话界面。此外,它还支持搭建本地服务器,供其他程序调用API。

3.Gemma 3模型

Gemini通常指Google的云端闭源模型,而Gemma 3系列模型,是其同源技术的开源版本,沿袭了Gemini的技术架构,在参数效率上表现出色,适合本地运行。

在M4 Pro上,推荐部署以下版本:

  • Gemma 3 12B:响应速度快,适合代码补全和日常对话。
  • Gemma 3 27B:逻辑推理能力更强,适合复杂的文本分析和创意写作,但要求内存在32GB及以上。

4.一些优势

  1. 隐私安全:数据无需上传云端。无论是文档、日记还是代码,所有推理过程均在本地完成,保障数据安全。
  2. 成本低廉:无需支付API调用费用或订阅费用,只需承担设备运行的电费,甚至可以白嫖公司电费。
  3. 离线可用:无网络时,AI助手依然在线,随时待命。

免费体验!开启吉卜力风格转换之旅

AI工具

0. 平台介绍

Ghiblio Art是一款专业的吉卜力风格AI图像生成平台,利用强大的ChatGPT 4o模型,将文字和照片转化为充满魔力的吉卜力插画。无论是柔和的水彩渐变、奇幻的建筑还是空灵的光效,Ghiblio Art都能创造出真实的吉卜力风格艺术作品。体验地址:https://ghiblio.art/?utm_source=7QB85D9QwaJe

1. 功能特性

  • 文生图与图生图:通过输入文字描述或上传图片生成吉卜力风格的插画。
  • 多种风格支持:除了吉卜力风格,还支持多种动漫风格。
  • 批量生图:支持多图生多图功能,满足更大规模的创作需求。

2.风格介绍

史努比风格

  • 特点:史努比风格以简单的线条和鲜明的色彩著称,通常展现轻松幽默的场景,非常适合儿童和家庭主题。
  • 示例图
    效果

吉卜力风格

  • 特点:吉卜力风格以细腻的笔触和丰富的色彩著称,常常展现自然与人类和谐共处的场景,充满梦幻与温暖。
  • 示例图
    效果

Q版风格

  • 特点:Q版风格以夸张的比例和可爱的形象为特点,通常用于表现卡通化和萌系角色,非常适合轻松可爱的主题。
  • 示例图
    效果

3. 使用体验

无需具备绘画技能,只需输入文字描述或上传图片,即可生成精美的多种风格图像。无论是艺术新手还是专业设计师,都能轻松创作。

4. 试用账号

充了50,发现用不完,有需要的自取。

账号:duanyangchn@qq.com

密码:ghiblioart

想注册自己账号的,也可用下面的邀请链接:https://ghiblio.art/?utm_source=7QB85D9QwaJe

探索Yourware.so:简化网页部署的创新平台

AI工具

0.前言

正吃瓜https://8zo2eorjmx.app.yourware.so,意外发现这个平台有点意思,可以一键部署网页。

想想以前,部署个网页也是费老劲了。云服务器得搞一个,这最最基本的。就按最简单的来,得在服务器上装个Nginx,然后丢个静态页到Web服务器根目录。

然后搞个个性域名,添加解析这都基本操作。服务器如果是国内的话,还得ICP备案和公安备案,用境外服务器倒可以省略这一步骤。

后来有了GitHub Pages,创建对应的仓库和指定名称的分支,就能直接访问到网页。配合GitHub Actions还能实现自动部署。但GitHub Pages仅支持静态网站。

再然后,又有Vercel这种的托管服务,前端后端都能托管。关联仓库后,当代码有更新时,Vercel直接自动拉取代码并部署。

现在,如果想快速呈现AI生成的代码效果。可以试试Yourware.so。

1.什么是Yourware.so

Yourware.so是一个专门用于上传和托管HTML或TSX文件的平台。它允许用户把人工智能生成的HTML或TSX代码上传到该平台,并将其转换为托管在云端的网页。用户只需上传、粘贴或拖放文件,就可以获得一个实时网站,无需任何配置和复杂的部署过程。

功能亮点

  1. 一键部署:访问Yourware.so,上传本地文件或粘贴代码后点击Deploy,等待片刻。

  2. 社区资源:Yourware.so提供了丰富的社区资源和便捷的管理功能,聚合了全球创作者的AI生成案例,激发灵感并推动AI创意生态。

  3. 支持AI创意:平台特别适合AI驱动的创意发展,用户可以通过AI生成的代码快速实现他们的想法。

2.总结

Yourware.so是一个强大的工具,旨在简化网页部署过程。它的出现为开发者提供了一个高效的平台来展示创意和项目。总之,快就完事了。

下来找找看有没有接口,也许可以搞个脚本直接上传本地复杂单页应用。

白嫖is-a.dev个性域名

0.前言

日常冲浪,发现可以白嫖两个个性域名。都是GitHub上的项目,is-a-dev和is-a-good-dev,顾名思义,一个可以注册.is-a.dev域名,另一个可以注册.is-a-good.dev域名。

1.注册步骤

首先,需要fork对应的GitHub仓库。

然后,在domains文件夹中创建一个名为your-domain-name.json的新文件,以注册your-domain-name.is-a.dev或your-domain-name.is-a-good.dev。

提交一个Pull request以供审核。审核通过后即可生效。

听起来很简单,操作也不复杂。但是,亲测is-a-dev的PR审核比较严苛。

两个项目下,我都是添加的CNAME解析,对应的json文件如下

{
  "owner": {
    "username": "Mrliduanyang",
    "email": "duanyangchn@gmail.com"
  },
  "record": {
    "CNAME": "duanyang.cool"
  },
  "proxied": false
}
{
  "owner": {
    "username": "Mrliduanyang",
    "email": "duanyangchn@gmail.com"
  },
  "target": {
    "CNAME": {
      "name": "duanyang",
      "value": "duanyang.cool"
    }
  },
  "proxied": false
}

再说is-a-dev的PR审核。

  • 一开始网页上放了个Hello World,不行,网页不完整,让修改。

  • 又让AI随便生成一个页面放上去,还不行,不完整,并且网页内容和开发无关,继续修改。

  • 没办法,找了之前写的几篇博客文章,用Hugo生成静态页,放上去,审核通过。

相比之下,is-a-good-dev的审核就宽松多了。

原因可能有两点吧。一是is-a-good-dev没有is-a-dev知名,从PR数量上也能反映,is-a-good-dev只有700+的PR,而is-a-dev有20000+;二可能和审核人员有关,他觉得行就给通过。

PR合并后,域名很快就生效,几乎没等多长时间。

2.最后

一直白嫖一直爽,希望项目不要倒了。感恩!

浏览器运行DeepSeek本地模型

DeepSeek 浏览器

0.前言

DeepSeek很火,毋庸置疑的火。一火吧,用的人就多,官网那个就卡,不然也不会有那么多第三方部署。也有很多本地部署的方案,比如基于Ollama部署。

但如果只是想尝尝鲜、玩一玩,不想折腾那么复杂的运行环境,那可以看看本文,直接在浏览器里运行DeepSeek。

1.浏览器中运行DeepSeek

为了在浏览器中高效运行DeepSeek,Hugging Face提供了transformers.js库,该库利用WebGPU技术来加速模型的推理过程。

同时,Hugging Face也提供了一系列适用于transformers.js的ONNX格式模型权重文件。

有运行时也有模型,开搞!

有NodeJS环境

推荐使用Node18,减少踩坑。

clone代码、run一把梭。

git clone https://github.com/huggingface/transformers.js-examples.git

cd transformers.js-examples/deepseek-r1-webgpu

npm i

npm run dev

在浏览器中输入http://localhost:5173打开页面,按操作提示加载模型后,即可和DeepSeek模型对话。

没有NodeJS环境

一点小小的限制:因为运行大模型是一个计算密集型的任务,为了不阻塞主线程,所以需要在worker线程中运行DeepSeek推理。

这就带来个新的问题,如果在本地双击html文件打开时,Web Worker会因为跨域限制而无法加载脚本。浏览器要求Web Worker的脚本必须通过 HTTP/HTTPS 协议加载,而不是直接从本地文件系统加载。

所以仍然需要一个HTTP服务器来提供文件服务。下载文件(https://github.com/Mrliduanyang/zhouzhou/blob/main/杂七杂八/dist.tar.gz),解压,然后在index.html所在路径下执行

# Mac和Linux系统都有基本的Python环境,Windows系统可自行下载安装Python环境

python3 -m http.server 5173

启动HTTP服务器。

在浏览器中输入http://localhost:5173打开页面,按操作提示加载模型后,即可和DeepSeek模型对话。

测试

在一台2019年,2.6 GHz 六核Intel Core i7的MacBook Pro上,速度可以达到7token/s。

在一台2021年,Apple M1 Pro的MacBook Pro上,速度可以达到34token/s。

简单问了一个数学题,效果如下:

效果

参考资料

https://github.com/huggingface/transformers.js-examples/tree/main/deepseek-r1-webgpu

手搓Mini Redis(Rust版)系列一

Redis Rust

0.前言

距上一篇分享LZW压缩算法已经过去了几个月了,是时候开新坑了。

最近花了点时间入门了一点Rust,在网上寻找学习资源的时候,发现了这个Tokio官方出的mini-redis,这是一个使用Rust实现的Redis服务端和客户端。但只实现了核心的GET、SET、PUBLISH、SUBSCRIBE功能。简单研究了一下,确实比较适合对初期学习到的各种Rust理念和操作进行一个巩固。

本期先分享mini-redis的数据结构部分。

1.mini-redis

mini-redis数据结构

一切从简,按照最简单最基本的方式来实现Redis。先来想一下,怎么存Redis中的键值对?最简单的方式就是用HashMap,所以先定义一个Redis数据状态类型:

struct State {
    entries: HashMap<String, Bytes>,
}

注意到Redis中的key是有过期时间的,所以我们需要修改下entries的值的类型,增加一个过期时间字段:

struct Entry {
    data: Bytes,
    expires_at: Option<Instant>,
}

struct State {
    entries: HashMap<String, Entry>,
}

这样看起来,存储键值对的部分就比较完善了。

接下来解决过期时间的问题,希望能有一种数据结构能直接取到最早过期的key,同时要考虑到多个key在同一时刻过期的问题,所以我们需要用BTreeSet加元组来存储过期时间,如下所示:

struct State {
    entries: HashMap<String, Entry>,
    expirations: BTreeSet<(Instant, String)>,
}

为什么是BTreeSet,首先BTree部分没啥争议,因为我们需要其中的元素有序,这样才能最快找到最早的过期时间。而Set是为了确保同一个过期时间+key的组合只有一个,想一下,存多个相同的过期时间+key组合也没什么意义。

这样一来,Redis数据存储部分的数据结构就设计完成了。

Redis中还有一项比较重要的功能:发布-订阅,我们来实现它。还得用HashMap,key是订阅的数据键,value则是一个消息发送者,当有一个key需要发送广播消息给订阅该key的订阅者时,直接取出发送者调用发送即可。所以我们进一步扩展State的结构:

struct State {
    entries: HashMap<String, Entry>,
    expirations: BTreeSet<(Instant, String)>,
    pub_sub: HashMap<String, broadcast::Sender<Bytes>>,
}

既然是用来存取数据的,我们要保证最起码的数据一致性,同一个数据,同一时刻,不能有多个写者。用最基本的互斥锁来实现就好,读写的时候都需要先获取锁,之后再进行读写,而没有获取到锁的则等待。我们在State结构上包一层,于是有了:

struct Shared {
    state: Mutex<State>,
}

了解Rust的应该清楚,Rust中通过所有权系统来保证内存安全,但所有权同时也限制了数据共享。而我们的Redis不可能一次只服务一个客户端,那要如何实现多线程之间的数据共享呢?也很简单,Rust提供了Arc(原子引用计数),这个结构通过Clone可以为共享的数据创建一个引用指针,同时增加引用计数器。所以我们在Shared结构上再包一层Arc,就有了:

struct Db {
    shared: Arc<Shared>,
}

数据结构之上的Redis操作

有了基础的数据结构定义后,我们来实现所需要的Redis能力。首先构造函数得有:

impl Db {
    pub(crate) fn new() -> Db {
        let shared = Arc::new(Shared {
            state: Mutex::new(State {
                entries: HashMap::new(),
                pub_sub: HashMap::new(),
                expirations: BTreeSet::new(),
            }),
        });

        Db { shared }
    }
}

get操作是一个纯读的操作,所以我们获取state的不可变引用后,获取互斥锁,然后通过HashMap的get方法来获取所需的值:

更多...
1 of 2 下一页