Merge pull request #16 from hanyixuanten/revert-15-next-gen

Revert "feat: 添加收藏夹选择和取消收藏功能"
This commit is contained in:
Searchstars
2026-07-03 21:58:42 +08:00
committed by GitHub
39 changed files with 85 additions and 738 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
/sign
/dist
/build
# .husky/ — hooks are tracked in version control
.husky/
*/__pycache__
src/buildinfo.ts
**/.DS_Store
-1
View File
@@ -1 +0,0 @@
npx --no -- commitlint --edit $1
-1
View File
@@ -1 +0,0 @@
npx lint-staged
@@ -1,252 +0,0 @@
# 代码修复计划 — Bug、风险与性能问题
## 修复优先级总览
| 阶段 | 问题数 | 描述 |
|------|--------|------|
| P0 紧急 | 8 | 功能完全不可用、崩溃、安全漏洞 |
| P1 高 | 8 | 数据损坏、启动失败、CI 失效 |
| P2 中 | 14 | 功能异常、性能退化、潜在隐患 |
| P3 低 | 11 | 代码质量、工具链现代化 |
---
## P0 紧急修复(功能不可用/崩溃)
### 1. 回复功能 — API 调用交换 + 未声明变量
**文件**: `src/pages/reply/replys/replys.ux`
- **L64-77**: `GiveTreeReply``GiveSecReply` 调用语义反了,交换调用
- **L97**: `this.input_text` 未声明,在 `private` data 中添加 `input_text: { content: "" }`
### 2. 音乐播放 — `api` 未定义
**文件**: `src/pages/video/player/player.ux`
- **L782,822,1261,1461,1495**: `api.getSongUrl()` / `api.getLyric()` 调用但 `api` 未导入
- 需要创建 `src/bilibiliclient/music/music.ts` 模块(或找到正确的 API 来源),在文件顶部导入并赋值给 `api`
### 3. 动态详情 — `typeof` 比较恒为 true
**文件**: `src/pages/app/features/dynamic/detail/detail.ux`
- **L87**: `typeof this.dyn != Object``typeof this.dyn !== "object"`
### 4. 空回复检查 — 字符串与数字比较
**文件**: `src/pages/video/videodetail/videodetail.ux`
- **L202**: `evt.detail.content === 0``!evt.detail.content || evt.detail.content.length === 0`
### 5. Settings JSON.parse 崩溃
**文件**: `src/settings.ts`
- **L37**: 用 try-catch 包裹 `JSON.parse(data)`catch 中保持默认 SETTINGS 并 log 警告
### 6. App 启动 Promise 无错误处理
**文件**: `src/app.ux`
- **L44-50**: 用 try-catch 包裹每个 await,或用 `Promise.allSettled` 并行执行非关键初始化
- 关键初始化(`savedcontent.initialize`)失败应显示错误页面
- 非关键初始化(`getNetworkType`)失败可降级为默认值
### 7. HTTP 追踪 URL(调试遗留)
**文件**: `src/usertracker.ts`
- **L5**: 取消注释 L4 的 HTTPS URL,删除或注释 L5 的 HTTP 内网地址
### 8. asyncapi `access()` 语义错误
**文件**: `src/asyncapi/file.ts`
- **L161**: `fail` 回调中 `reject(...)``resolve(false)`,使调用方能用布尔值判断文件是否存在
---
## P1 高优先级修复
### 9. API 响应空检查链
**文件**: `src/bilibiliclient/api/request.ts`
- **L60, L91**: `return response.data` 前增加对 `response``response.data` 的 null 检查
- 创建 `safeData(response)` 辅助函数,统一处理 `response.data.code !== 0` 的错误情况
- 在所有 bilibiliclient 子模块中使用此辅助函数
### 10. 文章图片空引用
**文件**: `src/articletools.ts`
- **L39**: 在访问 `dom.attributes.src` 前增加 `dom.attributes && dom.attributes.src` 检查
- **L49**: 第二个 `parseInt(dom.attributes.height)``parseInt(dom.attributes.width)`
### 11. 页面返回键崩溃
**文件**: `src/ui/ui.ts`
- **L55**: 增加 `pageStack.length >= 2` 判断,不足时跳过返回动画
- **L57-58**: 增加 `global.vmPool[lastPageName]` 存在性检查
### 12. VmPool GC 逻辑错误
**文件**: `src/ui/ui.ts`
- **L21**: `return``continue`
### 13. 构建脚本不传播失败
**文件**: `scripts/build_s3s4.py`, `scripts/build_rw5.py`
- **L8**: `os.system("yarn run build")``subprocess.run(["yarn", "run", "build"], check=True)`
- 同时更新 import 语句添加 `subprocess`
### 14. buildtools.py 异常吞掉
**文件**: `scripts/buildtools.py`
- **L18**: `Exception(...)``raise Exception(...)`
- **L27-31**: 用 `with open(...) as f:` 替换裸 `open()`
### 15. ESLint 配置缺失
**文件**: 新建 `eslint.config.mjs`
- 创建 ESLint flat config 文件,兼容 ESLint 9
- 或将 `package.json` 中 ESLint 降级到 `^8.x` 并保留旧 `.eslintrc`
### 16. Husky hooks 未版本控制 + API 过时
**文件**: `.gitignore`, `husky.sh`
-`.gitignore` 中删除 `.husky/`
- 用 Husky v9 API 重写 `husky.sh``husky init` + 直接写 `.husky/pre-commit``.husky/commit-msg` 脚本)
---
## P2 中优先级修复
### 17. bvid 默认值类型错误
**文件**: `src/pages/video/videodetail/videodetail.ux`
- **L84**: `bvid: 1``bvid: ""`
### 18. 数组初始化为对象
**文件**: `src/pages/user/user.ux` L53, `src/pages/search/search/search.ux` L32
- `masterpiece: {}``masterpiece: []`
- `hotwords: {}``hotwords: []`
### 19. 音频事件处理被覆盖
**文件**: `src/pages/video/player/player.ux`
- **L404-407**: 删除 `onInit` 中重复的 `audio.ontimeupdate` 赋值,或合并逻辑到 `bindAudioEvents`
### 20. fetchDefaultPlaylist 空函数
**文件**: `src/pages/video/player/player.ux`
- **L665-668**: 实现函数体或标记为 TODO 并添加用户提示
### 21. Settings 加载竞态
**文件**: `src/settings.ts`
- 重构 `loadSettings` 返回 `Promise`,在 `app.ux``await` 完成后再继续初始化
### 22. HTML 解析器嵌套标签问题
**文件**: `src/htmlparser.ts`
- **L38**: 对于嵌套标签,改用递归下降解析而非单正则匹配
- 或引入轻量 HTML 解析库
### 23. 收藏夹名称硬编码
**文件**: `src/bilibiliclient/video/action.ts` L23, L47
- 改用 `folder.id === 0`(B站默认收藏夹 ID 固定为 0)或通过 API 响应中的 `default` 字段判断
### 24. 存储索引无验证
**文件**: `src/savedcontent.ts`
- **L38**: JSON.parse 后增加 `Array.isArray()` 检查和元素结构验证
### 25. 日志无缓冲
**文件**: `src/logger/logger.ts`
- 实现内存缓冲区,批量写入(如每 10 条或每 5 秒 flush 一次)
- 添加日志文件轮转机制(保留最近 N 个文件)
### 26. VmPool GC 定时器优化
**文件**: `src/ui/ui.ts`
- 在页面销毁事件中触发 GC,替代 10 秒定时器
### 27. 动画引用泄漏
**文件**: `src/animation/engine.ts`
-`SequenceAnim.stop()``DomAnim.stop()` 中从 `global.animations` 移除自身引用
### 28. request.ts 无超时
**文件**: `src/bilibiliclient/api/request.ts`
- `getRequest``postRequest` 增加 `timeout` 参数(默认 15s),传入 `fetch.fetch()`
### 29. Content-Length 冗余设置
**文件**: `src/bilibiliclient/message/message.ts`
- **L51**: 删除手动 `Content-Length` 设置,让 HTTP 客户端自动处理
### 30. Stylelint 废弃规则
**文件**: `.stylelintrc.js`
- **L10-11**: 删除 `color-hex-case``color-hex-length`,或安装 `stylelint-stylistic` 插件
---
## P3 低优先级改进
### 31. formatNumber 边界处理
**文件**: `src/tools.ts` L3-13 — 增加 `NaN`/负数/undefined 守卫
### 32. funnytips 日期捕获时机
**文件**: `src/funnytips.ts` L1 — `new Date()` 移入 `getTips()` 函数内部
### 33. jumpcheck storage 无 fail 回调
**文件**: `src/jumpcheck.ts` — 增加 `fail` 回调,导航到错误页面
### 34. Object.assign 原型组装
**文件**: `src/bilibiliclient/client.ts` L55-68 — 长期重构为 class extends 或 mixin 模式(影响范围大,建议单独 PR)
### 35. quickapp.config.js 健壮性
**文件**: `quickapp.config.js`
- L7: `execSync` 包裹 try-catch
- L10: 相对路径 → `path.resolve(__dirname, ...)`
- L12-17: 模板字符串值转义引号和反斜杠
### 36. manifest.json 生产日志级别
**文件**: `src/manifest.json` L53 — `"log"``"warn"`(或在构建脚本中覆盖)
### 37. .prettierrc.js 废弃选项
**文件**: `.prettierrc.js` L10 — `jsxBracketSameLine``bracketSameLine`
### 38. buildInfoContent 注入风险
**文件**: `quickapp.config.js` L12-17 — 对插值值做 JSON.stringify 转义
### 39. PII 泄漏
**文件**: `quickapp.config.js` L8 — 移除 `os.userInfo().username`,改用 CI 环境变量或固定标识
### 40. logger %c 样式无效
**文件**: `src/logger/logger.ts` L61 — 移除 `%c` 和 style 参数
### 41. eula.ts 嵌套 `<p>` 标签
**文件**: `src/eula.ts` — 修正 HTML 结构,外层 `<p>` 改为 `<div>`
---
## 实施建议
### 分批策略
1. **第 1 批 (P0)**: 8 个修复,每个独立可测,建议逐个 PR
2. **第 2 批 (P1)**: 8 个修复,工具链相关可合并为一个 PR
3. **第 3 批 (P2)**: 14 个修复,按模块分组(player 模块、bilibiliclient 模块、基础设施)
4. **第 4 批 (P3)**: 11 个改进,可在后续迭代中逐步完成
### 验证方式
- 每个修复在 NuttX 设备或模拟器上手动验证对应功能
- 构建脚本修复:运行 `python scripts/build_s3s4.py``python scripts/build_rw5.py` 确认退出码
- 工具链修复:运行 `yarn lint` 确认 ESLint 正常工作
- CI 修复:推送到 `next-gen` 分支确认 GitHub Actions 流水线绿色
### 关键文件清单
| 文件 | 修改类型 |
|------|---------|
| `src/pages/reply/replys/replys.ux` | Bug 修复 |
| `src/pages/video/player/player.ux` | Bug 修复 + 功能实现 |
| `src/pages/app/features/dynamic/detail/detail.ux` | Bug 修复 |
| `src/pages/video/videodetail/videodetail.ux` | Bug 修复 |
| `src/settings.ts` | 健壮性修复 |
| `src/app.ux` | 错误处理 |
| `src/usertracker.ts` | 安全修复 |
| `src/asyncapi/file.ts` | 语义修复 |
| `src/bilibiliclient/api/request.ts` | 健壮性修复 |
| `src/articletools.ts` | Bug 修复 |
| `src/ui/ui.ts` | Bug 修复 + 性能 |
| `src/tools.ts` | 边界处理 |
| `src/htmlparser.ts` | Bug 修复 |
| `src/logger/logger.ts` | 性能优化 |
| `src/savedcontent.ts` | 健壮性修复 |
| `src/animation/engine.ts` | 内存泄漏修复 |
| `src/funnytips.ts` | Bug 修复 |
| `src/jumpcheck.ts` | 健壮性修复 |
| `src/bilibiliclient/video/action.ts` | 兼容性修复 |
| `src/bilibiliclient/message/message.ts` | 代码清理 |
| `scripts/build_s3s4.py` | CI 修复 |
| `scripts/build_rw5.py` | CI 修复 |
| `scripts/buildtools.py` | Bug 修复 |
| `quickapp.config.js` | 健壮性 + 安全 |
| `package.json` | 依赖更新 |
| `.prettierrc.js` | 配置更新 |
| `.stylelintrc.js` | 配置更新 |
| `husky.sh` | 重写 |
| `.gitignore` | 配置修复 |
| `.github/workflows/main.yml` | CI 修复 |
| `eslint.config.mjs` | 新建 |
| `src/pages/user/user.ux` | Bug 修复 |
| `src/pages/search/search/search.ux` | Bug 修复 |
| `src/eula.ts` | HTML 修正 |
| `src/manifest.json` | 配置调整 |
+1 -1
View File
@@ -7,7 +7,7 @@ module.exports = {
quoteProps: "consistent", // 要求对象字面量属性是否使用引号包裹,(‘as-needed’: 没有特殊要求,禁止使用,'consistent': 保持一致 , preserve: 不限制,想用就用)
trailingComma: "none", // 不添加对象和数组最后一个元素的逗号
bracketSpacing: false, // 对象中对空格和空行进行处理
bracketSameLine: false, // 在多行JSX元素的最后一行追加 >
jsxBracketSameLine: false, // 在多行JSX元素的最后一行追加 >
requirePragma: false, // 是否严格按照文件顶部的特殊注释格式化代码
insertPragma: false, // 是否在格式化的文件顶部插入Pragma标记,以表明该文件被prettier格式化过了
proseWrap: "preserve", // 按照文件原样折行
+2
View File
@@ -7,6 +7,8 @@ module.exports = {
ignoreFiles: ["node_modules", "test", "dist", "**/*.js"],
rules: {
"no-descending-specificity": null,
"color-hex-case": "lower",
"color-hex-length": "short",
"at-rule-no-unknown": null,
"block-no-empty": null,
"selector-pseudo-class-no-unknown": [
-67
View File
@@ -1,67 +0,0 @@
# AGENTS.md
## Project Overview
Third-party Bilibili client for Xiaomi Vela embedded watches, built with the **QuickApp** framework (快应用). Targets round watches (S3/S4 at 466px, RW5 at 432px design width).
## Key Commands
```bash
yarn # install deps (NEVER use npm — postinstall enforces this)
yarn start # dev server with watch + NuttX device
yarn build # production build (outputs dist/*.rpk)
yarn lint # eslint --fix on src/**/*.{ux,js}
python scripts/build_s3s4.py # build for S3/S4 watches (466px design width)
python scripts/build_rw5.py # build for RW5 watches (432px design width)
```
Build scripts mutate `src/manifest.json` `config.designWidth` before calling `yarn build`. The CI runs both sequentially.
## Architecture
**Framework**: QuickApp (小米快应用) — NOT Vue, React, or standard web. Uses `.ux` single-file components with `<template>`, `<script>`, `<style>` blocks. Docs: https://iot.mi.com/vela/quickapp
**Build toolchain**: `aiot-toolkit` (rspack-based). TypeScript compiled via `builtin:swc-loader`, NOT tsc. The `tsconfig.json` exists for editor support only.
**Entry point**: `src/manifest.json` → router entry is `pages/app/entry/splash`
**Generated file**: `src/buildinfo.ts` is auto-generated at build time (gitignored). Do not create or edit it manually. Import it as `$buildinfo`.
**System API bridge**: `src/tsimports.js` re-exports QuickApp system APIs (`@system.fetch`, `@system.storage`, etc.) for use in TypeScript files. Import from `'../tsimports'`, not directly from `@system.*`.
**Path aliases** (defined in `quickapp.config.js`):
- `@src``src/`
- `@components``src/components/`
- `@less``src/less/`
- `@protobuf``src/protobuf/`
- `$buildinfo``src/buildinfo.ts`
**BilibiliClient**: API client assembled via `Object.assign` onto a single prototype from modules in `src/bilibiliclient/`. Each subdirectory (video, search, comment, etc.) exports a methods object merged in `client.ts`.
**Global state**: App-level singletons are attached to `global` in `src/app.ux``global.biliclient`, `global.logger`, `global.settings`, `global.ui`, `global.animengine`, `global.bgimg`, `global.savedcontent`. Access these from any page/component.
## File Conventions
- **`.ux` files**: QuickApp components. Use `<import>` to register child components (not ES modules). Template syntax is Vue-like but has differences (e.g., `if`/`for` directives, `@click`).
- **`.ts` files**: Business logic, API clients, utilities. Standard TypeScript.
- **`.less` files**: Styles, scoped per component. Stylelint uses `postcss-less` custom syntax.
- **`src/less/`**: Shared style variables/mixins.
- **`src/common/`**: Shared static assets (images, icons).
## Style & Lint
- **Prettier**: no semicolons, double quotes, no trailing commas, 100 char width, no bracket spacing. `.ux` files parsed as Vue.
- **ESLint**: extends prettier config. Lints `.ux` and `.js` in `src/`.
- **Stylelint**: standard + recess property order. Ignores `.js` files. Allows QuickApp-specific properties (`placeholder-color`, `gradient-*`, `caret-color`, etc.).
- **lint-staged**: runs on commit via husky — prettier + eslint for `*.{ux,js}`, prettier + stylelint for `*.{less,css}`.
- **Commitlint**: conventional commits. Allowed types: `bug`, `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `revert`, `merge`.
## Gotchas
- **No test suite exists.** There are no unit tests or integration tests. Verify changes manually on device or emulator.
- **`src/manifest.json` is mutable at build time.** Build scripts overwrite `config.designWidth`. Don't assume its value is stable.
- **QuickApp is not web.** DOM APIs, `window`, `document` do not exist. The runtime is a V8/JSC engine on embedded Linux. Use `system.*` APIs for device capabilities.
- **`.ux` template syntax diverges from Vue.** For example, `static` attribute on elements marks them as non-reactive for perf. `@system.folme` provides animation, not CSS transitions.
- **i18n**: strings in `src/i18n/{en,zh}.json`. Components reference keys like `"main.title"`.
- **CI**: GitHub Actions builds on push/PR to `next-gen` branch only. Produces two RPK artifacts (S3/S4 and RW5).
- **No `dist/` or `build/` in repo.** Both are gitignored. RPK output goes to `dist/`.
-44
View File
@@ -1,44 +0,0 @@
import js from "@eslint/js"
import prettier from "eslint-config-prettier"
export default [
js.configs.recommended,
prettier,
{
files: ["src/**/*.{js,ux}"],
languageOptions: {
ecmaVersion: 2022,
sourceType: "module",
globals: {
global: "readonly",
console: "readonly",
setTimeout: "readonly",
setInterval: "readonly",
clearTimeout: "readonly",
clearInterval: "readonly",
Promise: "readonly",
JSON: "readonly",
Math: "readonly",
Date: "readonly",
String: "readonly",
Number: "readonly",
Array: "readonly",
Object: "readonly",
parseInt: "readonly",
parseFloat: "readonly",
isNaN: "readonly",
undefined: "readonly",
Uint8Array: "readonly",
requestAnimationFrame: "readonly",
cancelAnimationFrame: "readonly"
}
},
rules: {
"no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"no-console": "off"
}
},
{
ignores: ["dist/", "build/", "node_modules/", "src/buildinfo.ts"]
}
]
+3 -1
View File
@@ -1 +1,3 @@
npx husky init
npx husky install
npx husky add .husky/pre-commit 'npx lint-staged'
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit ${1}'
+1 -1
View File
@@ -11,7 +11,7 @@
"build": "aiot build --enable-jsc --enable-protobuf",
"release": "aiot release --enable-jsc --enable-protobuf",
"watch": "aiot watch --open-nuttx",
"lint": "eslint --format codeframe --fix src/",
"lint": "eslint --format codeframe --fix --ext .ux,.js src/",
"postinstall": "node -e \"if (process.env.npm_execpath && !process.env.npm_execpath.includes('yarn')) { console.error('Please use Yarn to install dependencies!'); process.exit(1); }\""
},
"lint-staged": {
+6 -11
View File
@@ -4,21 +4,16 @@ const os = require('os');
const fs = require('fs');
// 注入buildinfo
let gitCommitHash = "unknown";
try {
gitCommitHash = childProcess.execSync('git rev-parse HEAD').toString().trim();
} catch (e) {
console.warn('Failed to get git commit hash:', e.message);
}
const username = process.env.CI ? "ci" : os.userInfo().username;
const gitCommitHash = childProcess.execSync('git rev-parse HEAD').toString().trim();
const username = os.userInfo().username;
const buildTime = new Date().toISOString();
const designWidth = JSON.parse(fs.readFileSync(path.resolve(__dirname, "src/manifest.json"))).config.designWidth
const designWidth = JSON.parse(fs.readFileSync("src/manifest.json")).config.designWidth
const buildInfoContent = `
export const GIT_COMMIT_HASH = "${gitCommitHash.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}";
export const GIT_COMMIT_HASH = "${gitCommitHash}";
export const BUILD_TIME = "${buildTime}";
export const BUILD_USER = "${username.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}";
export const DESIGN_WIDTH = ${JSON.stringify(designWidth)};
export const BUILD_USER = "${username}";
export const DESIGN_WIDTH = ${designWidth};
`;
const buildInfoPath = path.resolve(__dirname, 'src/buildinfo.ts');
+2 -2
View File
@@ -1,8 +1,8 @@
import subprocess
import os
import buildtools
DESIGN_WIDTH = 432
buildtools.setManifestDesignWidth(DESIGN_WIDTH)
subprocess.run(["yarn", "run", "build"], check=True)
os.system("yarn run build")
+2 -2
View File
@@ -1,8 +1,8 @@
import subprocess
import os
import buildtools
DESIGN_WIDTH = 466
buildtools.setManifestDesignWidth(DESIGN_WIDTH)
subprocess.run(["yarn", "run", "build"], check=True)
os.system("yarn run build")
+5 -3
View File
@@ -15,7 +15,7 @@ def getGitCommitHash():
return commit_hash
else:
print("Failed to get commit hash. Make sure you are in a Git repository.")
raise Exception(f"Error: {result.stderr}")
Exception(f"Error: {result.stderr}")
except Exception as e:
print(f"An error occurred: {e}")
@@ -25,8 +25,10 @@ def getBuildTime():
return formatted_datetime
def readFileToJson(path):
with open(path, "r", encoding="utf-8") as f:
return json.loads(f.read())
f = open(path, "r")
f = f.read()
f = json.loads(f)
return f
def writeJsonToFile(path, obj):
with open(path, "w+", encoding="utf-8") as f:
+2 -18
View File
@@ -63,15 +63,7 @@ class SequenceAnim extends Anim {
}
stop(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
// Remove self from global.animations to prevent memory leak
if (global.animations && global.animations[this.pageId]) {
const idx = global.animations[this.pageId].indexOf(this);
if (idx !== -1) global.animations[this.pageId].splice(idx, 1);
}
clearInterval(this.intervalId!);
}
}
@@ -132,15 +124,7 @@ class DomAnim extends Anim {
}
stop(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
// Remove self from global.animations to prevent memory leak
if (global.animations && global.animations[this.pageId]) {
const idx = global.animations[this.pageId].indexOf(this);
if (idx !== -1) global.animations[this.pageId].splice(idx, 1);
}
clearInterval(this.intervalId!);
}
}
+5 -28
View File
@@ -42,39 +42,16 @@ export default {
global.settings = settings
console.log("appinit > initSavedContentManager")
try {
await savedcontent.SavedContentManager.initialize()
} catch (e) {
console.error("Failed to init SavedContentManager:", e)
}
await savedcontent.SavedContentManager.initialize()
global.savedcontent = savedcontent
console.log("appinit > initDeviceInfo")
try {
global.DEVICE_INFO = await tools.getDeviceInformation()
} catch (e) {
console.error("Failed to get device info:", e)
global.DEVICE_INFO = { screenShape: "round", product: "unknown" }
}
try {
global.DEVICE_SERIAL = await tools.getDeviceSerial()
} catch (e) {
console.error("Failed to get device serial:", e)
global.DEVICE_SERIAL = "unknown"
}
try {
global.DEVICE_NETWORK_TYPE = await tools.getNetworkType()
} catch (e) {
console.error("Failed to get network type:", e)
global.DEVICE_NETWORK_TYPE = "unknown"
}
global.DEVICE_INFO = await tools.getDeviceInformation()
global.DEVICE_SERIAL = await tools.getDeviceSerial()
global.DEVICE_NETWORK_TYPE = await tools.getNetworkType()
console.log("appinit > initBgImg")
try {
this.bgimg.Init()
} catch (e) {
console.error("Failed to init bgimg:", e)
}
this.bgimg.Init()
global.bgimg = this.bgimg
console.log("appinit > createBiliClient")
+1 -2
View File
@@ -35,7 +35,6 @@ export function PatchArticleContent(doms: any) {
if (!dom) continue; // 避免空节点
if (dom.type === "img") {
if (!dom.attributes || !dom.attributes.src) continue;
// 如果图片的 src 包含 .png 或 .jpg 扩展名
if (dom.attributes.src.includes(".png") || dom.attributes.src.includes(".jpg")) {
// 确保 src 包含 http:// 或 https:// 前缀
@@ -47,7 +46,7 @@ export function PatchArticleContent(doms: any) {
if(dom.attributes){
// 如果图片过大,调整其尺寸
if (parseInt(dom.attributes.height) > 500 || parseInt(dom.attributes.width) > 500) {
if (parseInt(dom.attributes.height) > 500 || parseInt(dom.attributes.height) > 500) {
dom.attributes.src += "@250h"; // 对应补丁操作
dom.attributes._patched_sign = "large_picture_scaled"
}
+1 -1
View File
@@ -158,7 +158,7 @@ class FileAPI {
file.access({
...options,
success: () => resolve(true),
fail: () => resolve(false),
fail: (_, code: number) => reject(new Error(`Access failed with code ${code}`)),
});
});
}
+2 -6
View File
@@ -55,10 +55,8 @@ export const BilibiliClientAPIRequestMethods = {
const response = await this.fetch.fetch({
url,
responseType: responseType,
header: this.getHeaders(),
timeout: 15000
header: this.getHeaders()
});
if (!response || !response.data) return void 0;
return response.data;
} catch (error) {
global.logger.error(`GET请求失败,详细数据:`, error);
@@ -88,10 +86,8 @@ export const BilibiliClientAPIRequestMethods = {
responseType: 'json',
method: 'POST',
data,
header: headers,
timeout: 15000
header: headers
});
if (!response || !response.data) return void 0;
return response.data;
} catch (error) {
global.logger.error(`POST请求失败,详细数据:`, error);
-8
View File
@@ -7,14 +7,6 @@ export const BilibiliClientFavFolderMethods = {
return response.data.data;
},
// 获取用户所有收藏夹及目标视频在各收藏夹中的收藏状态
// 返回的每个 folder 对象中包含 fav_state 字段:0=未收藏, 1=已收藏
async getUserFavouriteFoldersWithVideoState(this: any, mid: string, aid: string): Promise<any> {
const url = `https://api.bilibili.com/x/v3/fav/folder/created/list-all?up_mid=${mid}&type=2&rid=${aid}`;
const response = await this.getRequest(url);
return response.data.data;
},
// 获取目标收藏夹元数据
async getFavouriteFolderMetadata(this: any, mlid: string): Promise<any> {
const url = `https://api.bilibili.com/x/v3/fav/folder/info?media_id=${mlid}`;
+2
View File
@@ -48,6 +48,8 @@ export const BilibiliClientMessageMethods = {
headers["Host"] = "api.vc.bilibili.com"
headers["Origin"] = "https://message.bilibili.com"
headers["Referer"] = "https://message.bilibili.com/"
headers["Content-Length"] = body.length.toString()
const response = await this.postRequestWbi(url, {
w_sender_uid: this.accountInfo.mid,
w_receiver_id: receiver_id,
+1 -53
View File
@@ -20,7 +20,7 @@ export const BilibiliClientVideoActionMethods = {
let defaultFolderID = 0;
const folders = await this.getUserFavouriteFolders(this.accountInfo.mid);
folders.list.forEach((folder: any) => {
if (folder.attr === 1 || folder.id === 0) {
if (folder.title === "默认收藏夹") {
defaultFolderID = folder.id;
}
});
@@ -38,56 +38,4 @@ export const BilibiliClientVideoActionMethods = {
return false;
}
},
// 取消收藏视频从默认收藏夹
async unstarVideoFromDefaultFavFolderByBVID(this: any, bvid: string): Promise<any> {
let defaultFolderID = 0;
const folders = await this.getUserFavouriteFolders(this.accountInfo.mid);
folders.list.forEach((folder: any) => {
if (folder.attr === 1 || folder.id === 0) {
defaultFolderID = folder.id;
}
});
if (!defaultFolderID) return false;
try {
const videoInfo = await this.getVideoInfoByBVID(bvid);
const aid = videoInfo.aid;
const url = `https://api.bilibili.com/x/v3/fav/resource/deal`;
const data = `rid=${aid}&csrf=${this.biliJct}&type=2&add_media_ids=&del_media_ids=${defaultFolderID}`;
const response = await this.postRequest(url, data, "application/x-www-form-urlencoded");
return response.data.code;
} catch (error) {
global.logger.error("Error unstarring video: ", error);
return false;
}
},
// 收藏视频至指定收藏夹(支持多个)
async starVideoToFavFolders(this: any, aid: string, folderIds: string[]): Promise<any> {
try {
const add_media_ids = folderIds.join(',');
const url = `https://api.bilibili.com/x/v3/fav/resource/deal`;
const data = `rid=${aid}&csrf=${this.biliJct}&type=2&add_media_ids=${add_media_ids}&del_media_ids=`;
const response = await this.postRequest(url, data, "application/x-www-form-urlencoded");
return response.data.code;
} catch (error) {
global.logger.error("Error starring video to folders: ", error);
return false;
}
},
// 从指定收藏夹取消收藏视频(支持多个)
async unstarVideoFromFavFolders(this: any, aid: string, folderIds: string[]): Promise<any> {
try {
const del_media_ids = folderIds.join(',');
const url = `https://api.bilibili.com/x/v3/fav/resource/deal`;
const data = `rid=${aid}&csrf=${this.biliJct}&type=2&add_media_ids=&del_media_ids=${del_media_ids}`;
const response = await this.postRequest(url, data, "application/x-www-form-urlencoded");
return response.data.code;
} catch (error) {
global.logger.error("Error unstarring video from folders: ", error);
return false;
}
},
}
+10 -10
View File
@@ -5,24 +5,24 @@ export const eula = ` <h1>澎湃哔哩(Hyperbili)用户协议</h1>
<p>本客户端是基于哔哩哔哩网页端API开发的第三方工具,旨在为用户提供在部分搭载Xiaomi Vela OS的设备上浏览哔哩哔哩提供的内容的能力。本客户端不具备独立的账号系统,用户需通过哔哩哔哩官方App进行扫码登录。所有的服务和内容均来自哔哩哔哩平台,我们不对其内容的准确性、合法性或其他方面负责。</p>
<h2>2. 免责声明</h2>
<div>
<p>
<p><strong>服务稳定性</strong>:我们将尽力维护客户端的正常运行,但不保证其无故障、无中断或随时可用。由于哔哩哔哩API变更、网络故障、服务器问题或其他不可抗力因素导致的服务中断、数据丢失或功能异常,AstralSight Studios不承担任何责任。</p>
<p><strong>第三方内容</strong>:本客户端依赖哔哩哔哩平台提供的内容,AstralSight Studios不对任何第三方内容的准确性、合法性、完整性或适用性做出任何承诺或保证,用户需自行判断并承担使用风险。</p>
<p><strong>责任限制</strong>:在任何情况下,AstralSight Studios不对用户因使用或无法使用本客户端导致的任何间接、附带、特殊或惩罚性损害承担责任。</p>
<p><strong>不可抗力</strong>:对于因自然灾害、战争、网络攻击、政府行为、哔哩哔哩API关闭或变更等不可抗力事件造成的服务中断或损失,AstralSight Studios不承担任何责任。</p>
</div>
</p>
<h2>3. 账号安全与使用</h2>
<p>用户通过哔哩哔哩官方App扫码登录并使用其哔哩哔哩账号。本客户端不会保存用户登录密码,但会保存用户的登录Token、与哔哩哔哩API交互产生的Cookies等,但不对其安全性(包括但不限于:存储安全性、传输安全性)负任何责任。用户需对其账号安全负责。如果因用户原因导致账号被盗或发生其他安全问题,AstralSight Studios不承担责任。</p>
<h2>4. 用户行为规范</h2>
<p>用户在使用本客户端时应遵守国家法律法规、哔哩哔哩平台的相关规定,以及本协议中的条款。用户不得利用本客户端进行包括但不限于以下行为:</p>
<div>
<p>
<p>传播非法、侵权、骚扰、诽谤、淫秽、欺诈性内容;</p>
<p>侵犯他人知识产权、隐私权、名誉权等合法权益;</p>
<p>进行批量数据抓取、自动化访问或未经授权的网络攻击;</p>
<p>任何其他违反哔哩哔哩平台规则或法律法规的行为。</p>
</div>
</p>
<p>对于上述行为,AstralSight Studios有权立即中止或终止用户的使用权限,且不承担任何法律责任。</p>
<h2>5. 开源许可</h2>
@@ -39,29 +39,29 @@ export const eula = ` <h1>澎湃哔哩(Hyperbili)用户协议</h1>
<h2>1. 信息的收集</h2>
<p>在使用本客户端的过程中,我们可能会收集但不限于以下信息:</p>
<div>
<p>
<p><strong>设备信息</strong>:包括设备型号、操作系统、设备标识符等;</p>
<p><strong>网络信息</strong>:包括IP地址、网络提供商、连接时长等;</p>
<p><strong>操作日志</strong>:包括用户操作行为、功能使用情况、访问页面记录等;</p>
<p><strong>其他数据</strong>:随着产品功能的扩展,我们可能收集更多种类的用户数据,具体收集内容将在未来版本的隐私政策中进行更新。</p>
</div>
</p>
<h2>2. 数据的使用</h2>
<p>我们将收集到的用户数据用于以下目的:</p>
<div>
<p>
<p>优化和改进客户端的功能与用户体验;</p>
<p>分析用户行为,提升产品的使用效果;</p>
<p>保障客户端的安全,防止恶意攻击和非法活动;</p>
<p>满足法律法规要求,或配合相关部门的合法请求。</p>
</div>
</p>
<h2>3. 数据的共享</h2>
<p>我们不会主动将用户的个人数据分享给任何第三方,除非在以下情形下:</p>
<div>
<p>
<p>因任何不可抗力(包括但不限于 因资金短缺造成无法负担起数据存储费用、遭受网络攻击等)导致数据必须分享给第三方以进行包括但不限于数据存储、CDN分发、异地容灾等操作;</p>
<p>遵守法律法规的要求,或响应执法机关、法院的合法请求;</p>
<p>在维护客户端的合法权益时,为保护AstralSight Studios免受潜在侵害,可能会与相关合作伙伴或技术提供商共享必要数据。</p>
</div>
</p>
<h2>4. 数据的保护</h2>
<p>我们将采取合理的技术手段和管理措施,保护用户数据免受未经授权的访问、泄露、篡改或破坏。然而,互联网环境中无法完全消除所有风险,用户应知晓并承担这些潜在风险。AstralSight Studios不对因用户行为或外部因素导致的数据泄露或损失负责。</p>
+2 -1
View File
@@ -1,3 +1,5 @@
const date = new Date()
const tips = [
"你所热爱的,就是你热爱的",
"让我猜猜你现在使用的设备是...?",
@@ -30,7 +32,6 @@ const AprilFoolsDayTips = [
]
export function getTips(): string{
const date = new Date()
// 愚人节特供
if(date.getMonth() === 3 && date.getDate() === 1){
return AprilFoolsDayTips[Math.floor(Math.random() * AprilFoolsDayTips.length)]
+1 -6
View File
@@ -4,7 +4,7 @@ export async function Jump() {
storage.get({
key: "bilibili_account",
success: async (bilibili_account) => {
if (!bilibili_account || bilibili_account.length < 1) {
if (bilibili_account.length < 1) {
router.replace({
uri: "pages/app/entry/login"
})
@@ -13,11 +13,6 @@ export async function Jump() {
uri: "pages/app/entry/prepage"
})
}
},
fail: function () {
router.replace({
uri: "pages/app/entry/login"
})
}
})
}
+1 -1
View File
@@ -58,7 +58,7 @@ class Logger {
const message = args.map((arg) => this.safeStringify(arg)).join(' ');
// 在控制台输出日志
console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
console.log(`%c[${timestamp}] [${level.toUpperCase()}] ${message}`, style);
// 同步日志到文件
this.flushLogToFile(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
+1 -1
View File
@@ -50,7 +50,7 @@
}
],
"config": {
"logLevel": "warn",
"logLevel": "log",
"designWidth": "device-width",
"background": {
"features": [
@@ -84,7 +84,7 @@ export default {
onInit() {
// 来自子组件push的object传递会变成字符串
// 因此需要进行类型检查和手动parse
if (typeof this.dyn !== "object") {
if (typeof this.dyn != Object) {
this.dyn = JSON.parse(this.dyn)
console.log(this.dyn)
}
+8 -9
View File
@@ -48,8 +48,7 @@ export default {
},
replymode: false,
reply_target: null,
scrclass: "",
input_text: { content: "" }
scrclass: ""
},
async SendReply(evt) {
this.replymode = false
@@ -63,19 +62,19 @@ export default {
let result
try {
result = this.secmode
? await global.biliclient.GiveSecReply(
this.type,
this.oid,
this.reply_target,
content
)
: await global.biliclient.GiveTreeReply(
? await global.biliclient.GiveTreeReply(
this.type,
this.oid,
this.reply_target,
this.rootrpid,
content
)
: await global.biliclient.GiveSecReply(
this.type,
this.oid,
this.reply_target,
content
)
if (result.code === 0) {
prompt.showToast({message: "发送成功"})
+1 -1
View File
@@ -29,7 +29,7 @@
import router from "@system.router"
export default {
private: {
hotwords: [],
hotwords: {},
showHotwordsList: false,
input_mode: false,
anims: {
+1 -1
View File
@@ -50,7 +50,7 @@ export default {
user: {},
stat: {},
navnum: {},
masterpiece: [],
masterpiece: {},
dynamiclist: {},
anims: {
show_loading: false
+4 -15
View File
@@ -138,18 +138,6 @@
import prompt from "@system.prompt";
import request from "@system.request";
import { asyncFile } from "../../../asyncapi/file";
// Music API stub — getSongUrl/getLyric are not yet implemented.
// Wrapping in a safe object prevents ReferenceError crashes.
const api = {
getSongUrl(id, opts) {
if (opts && opts.fail) opts.fail('Music API not implemented');
},
getLyric(id, opts) {
if (opts && opts.fail) opts.fail('Music API not implemented');
}
};
export default {
props: { playlistId: { default: null }, startIndex: { default: 0 }, tracks: { default: null }, bvid: { default: null }, cachedAudioUri: { default: null }, cachedTitle: { default: null }, cachedCoverUrl: { default: null }, cachedAuthor: { default: null } },
data: {
@@ -413,9 +401,10 @@
that.audioReady = true;
}
};
// ontimeupdate is already set in bindAudioEvents() with pendingSongId guard.
// Do NOT overwrite it here — the bindAudioEvents version handles both
// playerState.playDuration and updateLyric().
audio.ontimeupdate = () => {
that.progress = audio.percent;
that.updateLyric();
};
if (hasNewPlaylist || !fromAppStart) {
if (that.videoMode) {
} else {
@@ -52,73 +52,4 @@
.vidtoolimg {
width: 62px;
}
.fav-folder-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
flex-direction: column;
justify-content: center;
align-items: center;
}
.fav-folder-picker {
width: 340px;
height: 380px;
background-color: #2a2a2a;
border-radius: 16px;
flex-direction: column;
align-items: center;
padding: 20px;
}
.fav-folder-picker-title {
font-size: 22px;
font-weight: 700;
color: #ffffff;
margin-bottom: 15px;
}
.fav-folder-list {
width: 100%;
height: 260px;
flex-direction: column;
}
.fav-folder-item {
flex-direction: row;
align-items: center;
padding: 10px 8px;
margin-bottom: 4px;
border-radius: 8px;
background-color: #3a3a3a;
}
.fav-folder-item-selected {
background-color: #4a6fa5;
}
.fav-folder-checkmark {
font-size: 20px;
color: #ffffff;
margin-right: 10px;
width: 28px;
text-align: center;
}
.fav-folder-name {
font-size: 18px;
color: #ffffff;
flex: 1;
}
.fav-folder-buttons {
flex-direction: row;
justify-content: space-between;
width: 100%;
margin-top: 15px;
}
+6 -96
View File
@@ -54,22 +54,6 @@
</scroll>
<full-screen-input style="position: absolute" if="{{replymode}}" @send="SendReply"
@exit="ExitInput"></full-screen-input>
<div class="fav-folder-overlay" if="{{favFolderPickerVisible}}">
<div class="fav-folder-picker">
<text class="fav-folder-picker-title">选择收藏夹</text>
<scroll scroll-y="true" class="fav-folder-list">
<div class="fav-folder-item {{folder.selected ? 'fav-folder-item-selected' : ''}}"
for="folder in favFolders" @click="ToggleFolder(folder.id)">
<text class="fav-folder-checkmark">{{folder.selected ? '✓' : '○'}}</text>
<text class="fav-folder-name">{{folder.title}}</text>
</div>
</scroll>
<div class="fav-folder-buttons">
<default-button text="取消" @click="HideFavFolderPicker"></default-button>
<default-button text="确认" @click="ConfirmAddToFav"></default-button>
</div>
</div>
</div>
</div>
</template>
@@ -81,7 +65,7 @@ import {kmeansCircleGradientImage} from "@src/image/jpegkmeans.ts"
export default {
public: {
bvid: "" // 视频BVID
bvid: 1 // 视频BVID
},
private: {
vid: {},
@@ -93,9 +77,7 @@ export default {
starsrc: "/common/vidtool_star.png",
replymode: false,
renderingCover: false,
scrclass: "",
favFolderPickerVisible: false,
favFolders: []
scrclass: ""
},
computed: {
stat_view() {
@@ -199,7 +181,7 @@ export default {
},
async SendReply(evt) {
this.replymode = false
if (!evt.detail.content || evt.detail.content.length === 0) {
if (evt.detail.content === 0) {
return prompt.showToast({
message: "评论内容不得为空!",
duration: 2000
@@ -238,83 +220,11 @@ export default {
})
},
async AddToFav() {
if (this.stared) {
// 已收藏:从所有包含此视频的收藏夹中删除
await this.ConfirmUnstarAll()
} else {
// 未收藏:弹出收藏夹选择窗口
await this.ShowFavFolderPicker()
}
},
async ShowFavFolderPicker() {
try {
var self = this
var data = await global.biliclient.getUserFavouriteFoldersWithVideoState(
global.biliclient.accountInfo.mid,
this.vid.aid
)
var list = data.list || []
this.favFolders = list.map(function(folder) {
return {
id: folder.id,
title: folder.title,
selected: folder.fav_state === 1
}
})
this.favFolderPickerVisible = true
} catch (e) {
global.logger.error("获取收藏夹列表失败: ", e)
prompt.showToast({message: "获取收藏夹列表失败,请重试"})
}
},
HideFavFolderPicker() {
this.favFolderPickerVisible = false
},
ToggleFolder(folderId) {
this.favFolders = this.favFolders.map(function(f) {
if (f.id === folderId) {
return Object.assign({}, f, { selected: !f.selected })
}
return f
})
},
async ConfirmAddToFav() {
var selectedIds = this.favFolders
.filter(function(f) { return f.selected })
.map(function(f) { return f.id })
this.favFolderPickerVisible = false
if (selectedIds.length === 0) {
prompt.showToast({message: "未选择任何收藏夹"})
return
}
var result = await global.biliclient.starVideoToFavFolders(this.vid.aid, selectedIds)
var message = result === 0 ? "收藏成功" : "收藏失败,请重试,或重新登录后再试"
prompt.showToast({message: message})
const result = await global.biliclient.starVideoToDefaultFavFolderByBVID(this.bvid)
const message = result ? "收藏失败,请重试,或重新登录后再试" : "收藏成功"
prompt.showToast({message})
this.UpdateVideoToolbarStatus()
},
async ConfirmUnstarAll() {
try {
var data = await global.biliclient.getUserFavouriteFoldersWithVideoState(
global.biliclient.accountInfo.mid,
this.vid.aid
)
var list = data.list || []
var staredFolderIds = list
.filter(function(f) { return f.fav_state === 1 })
.map(function(f) { return f.id })
if (staredFolderIds.length === 0) {
prompt.showToast({message: "该视频未被收藏"})
return
}
var result = await global.biliclient.unstarVideoFromFavFolders(this.vid.aid, staredFolderIds)
var message = result === 0 ? "取消收藏成功" : "取消收藏失败,请重试,或重新登录后再试"
prompt.showToast({message: message})
this.UpdateVideoToolbarStatus()
} catch (e) {
global.logger.error("取消收藏失败: ", e)
prompt.showToast({message: "取消收藏失败,请重试"})
}
},
async GenBlur(evt) {
// 该Feature在现在任何设备上运行都会卡死,故注释
/*
+1 -2
View File
@@ -35,8 +35,7 @@ async function loadStorageIndex(): Promise<void> {
const fileExists = await asyncFile.access({ uri: indexFileUri });
if (fileExists) {
const indexData = await asyncFile.readText({ uri: indexFileUri });
const parsed = JSON.parse(indexData);
storageIndex = Array.isArray(parsed) ? parsed : [];
storageIndex = JSON.parse(indexData);
} else {
storageIndex = [];
}
+5 -9
View File
@@ -34,15 +34,11 @@ export function loadSettings(): void {
key: 'settings',
success: function (data) {
if (data) {
try {
const storedSettings = JSON.parse(data);
SETTINGS = {
...SETTINGS,
...storedSettings
};
} catch (e) {
global.logger.warn('Failed to parse settings, using defaults:', e);
}
const storedSettings = JSON.parse(data);
SETTINGS = {
...SETTINGS,
...storedSettings
};
}
global.logger.log('Settings loaded:', SETTINGS);
},
-2
View File
@@ -1,8 +1,6 @@
import { device, network, router } from "./tsimports"
export function formatNumber(num: number): string {
if (num == null || isNaN(num)) return "0";
if (num < 0) return "0";
if (num < 1000) {
return num.toString();
}
+4 -9
View File
@@ -18,7 +18,7 @@ function runVmPoolGC(){
for(var name in global.vmPool){
if(global.vmPool[name]){
if(global.vmPool[name].$valid){
continue
return
}
}
@@ -46,20 +46,15 @@ export function InitPage(vm){
export function OnBackPressTriggered(){
GlobalActions.UpdateCurrentPageName()
if (global.vmPool[global.currentPageName]) {
global.vmPool[global.currentPageName].scrclass = "scroll-backanim"
}
global.vmPool[global.currentPageName].scrclass = "scroll-backanim"
setTimeout(() => {
GlobalActions.ClearCurrentVmSrcClass()
router.back()
var pageStack = router.getPages()
if (pageStack.length < 2) return
var lastPageName = pageStack[pageStack.length - 2].name
global.logger.log("lastpage=", lastPageName, "currentPage=", global.currentPageName)
if (global.vmPool[lastPageName]) {
global.vmPool[lastPageName].scrclass = ""
global.vmPool[lastPageName].scrclass = "scroll-frombackanim"
}
global.vmPool[lastPageName].scrclass = ""
global.vmPool[lastPageName].scrclass = "scroll-frombackanim"
}, 150)
}
+2 -2
View File
@@ -1,8 +1,8 @@
import dayjs from "dayjs"
import { fetch } from "./tsimports"
const TRACKER_URL: string = "https://tracker.hyperbili.astralsight.space/trackreport"
// const TRACKER_URL: string = "http://192.168.1.247:4080/trackreport"
//const TRACKER_URL: string = "https://tracker.hyperbili.astralsight.space/trackreport"
const TRACKER_URL: string = "http://192.168.1.247:4080/trackreport"
const TRACKER_SERVER_PROTOCOL_VERSION = "v1"
interface TrackedInfoUploadPacket {